UE4:移动端级联阴影简要分析

前言

假装过一下移动端渲染管线的源码,其实是看一下移动端的固定方向光源对移动物体生成阴影的部分。
本文重点在UE4 关于CSM阴影的这部分源码,其他的略过
之前从来没有看过这部分源码,本人基础也很薄弱,rendering方面能力不足,本文仅仅是给我的脑子梳理一下,避免稀里糊涂的看完不出三天就忘记了……
学而不思则罔~


目录:


流程梳理

函数
类或结构体
变量

简单描述

简单的文字描述一下UE4 CSM实现大概的流程,防止本文作者出现猪脑过载迹象……

  1. 移动端的主渲染流程开始
  2. 渲染进入初始化View阶段
  3. 初始化View中有一个阶段是生成动态阴影
  4. 生成动态阴影的其中一步是创建全场景投影阴影,就是生成CSM阴影

生成CSM的流程:

  1. 确认级联的数量
  2. 对每一个级联生成投影阴影信息
  3. 据阴影数据构造灯光视锥体和灯光变换矩阵
  4. 确认物体是否在灯光视锥中产生阴影,并确认阴影落在了第几级的级联
  5. 分配CSM深度Target,但是在移动端会延迟分配的时机以便确认是否可以将聚光灯阴影与之结合

详细流程

  • 1、 FMobileSceneRendererRender函数,是移动端渲染的主流程函数
  • 2、 Render函数中调用了InitViews函数
  • 3、 InitViews函数调用了FMobileSceneRenderer::InitDynamicShadows函数,为每一个View寻找可见的动态阴影,该函数又调用了FSceneRenderer::InitDynamicShadows
  • 4、 InitDynamicShadows 函数中AddViewDependentWholeSceneShadowsForView 处理了CSM相关数据,该函数生成了指定灯光的所有wholesceneshadows的FProjectedShadowInfo
    • 4.1、 确认是否需要为View创建whole scene view dependent shadow
    • 4.2、 GetNumViewDependentWholeSceneShadows 函数确认级联的数量
    • 4.3、 根据级联数量开始对每一个级联进行处理
      • 4.3.1、 声明FWholeSceneProjectedShadowInitializer类型的一个变量
      • 4.3.2、 通过GetViewDependentWholeSceneProjectedShadowInitializer函数对上面那个变量填充其成员
        • 4.3.2.1、 GetShadowSplitBounds计算该级联部分的视锥体的包围球
          • 4.3.2.1.1、 通过神奇的方式算出来了这个球的中心和半径,得到了包围球
          • 4.3.2.1.2、 计算阴影裁剪体ComputeShadowCullingVolume
            • 4.3.2.1.2.1、 计算出了一个凸多面体
              该投影裁剪体在后续的GatherDynamicMeshElements 的过程中有使用到
      • 4.3.3、 创建投影阴影信息
        • 4.3.3.1、 声明FProjectedShadowInfo变量
        • 4.3.3.2、 通过SetupWholeSceneProjection函数对上面那个变量进行成员填充,需要用到前面创建的FWholeSceneProjectedShadowInitializer
          • 4.3.3.2.1、 填充了各种变量
          • 4.3.3.2.1、 通过神奇的方式更新了PreShadowTranslation,说是可以避免
  • 5、 InitProjectedShadowVisibility,计算投影阴影的可见性
  • 6、 UpdatePreshadowCache,清除旧的preshadows并尝试向缓存中添加新的
  • 7、 GatherShadowPrimitives,收集用于绘制各种阴影类型的图元列表
  • 8、 AllocateShadowDepthTargets,分配阴影深度渲染目标
    • 8.1、 非移动端会在这里面调用AllocateCSMDepthTargets,但是移动端会MobileWholeSceneDirectionalShadows.Append(WholeSceneDirectionalShadows)延迟分配时机
    • 8.2、 AllocateMobileCSMAndSpotLightShadowDepthTargets(RHICmdList, MobileWholeSceneDirectionalShadows)进行分配
  • 9、 GatherShadowDynamicMeshElements,从阴影图元数组生成网格元素数组

源码学习

移动端渲染主函数

那么我们首先来到移动端渲染代码的地方:FMobileSceneRenderer
位于SceneRendering.h文件中

1
2
3
4
5
6
7
8
9
10
// Renderer that implements simple forward shading and associated features.
class FMobileSceneRenderer : public FSceneRenderer
{
// ....

// Renders the view family.
virtual void Render(FRHICommandListImmediate& RHICmdList) override;

// ....
}

那么我们直奔Render函数,看看里面都要干啥

1
2
3
4
5
6
7
8
9
10
11
12
/** 
* Renders the view family.
*/
void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
// ...

// Find the visible primitives and prepare targets and buffers for rendering
InitViews(RHICmdList);

// ...
}

移动端InitViews

别的步骤暂时略过,发现InitViews里处理了dynamic shadows:

1
2
3
4
5
6
7
8
9
10
11
12
const bool bDynamicShadows = ViewFamily.EngineShowFlags.DynamicShadows;

if (bDynamicShadows && !IsSimpleForwardShadingEnabled(ShaderPlatform))
{
// Setup dynamic shadows.
InitDynamicShadows(RHICmdList);
}
else
{
// TODO: only do this when CSM + static is required.
PrepareViewVisibilityLists();
}

看上面的意思就是,开启了动态阴影的选项,并且不是简单前向渲染的话,是要InitDynamicShadows的,那么本文的主要目标就出现了,着重看一下FMobileSceneRenderer::InitDynamicShadows

移动端初始化动态阴影

移动端初始化动态阴影的函数

FMobileSceneRenderer::InitDynamicShadows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/** Finds the visible dynamic shadows for each view. */
// 查找每个视图的可见动态阴影。
void FMobileSceneRenderer::InitDynamicShadows(FRHICommandListImmediate& RHICmdList)
{
// 这个值决定了Primitive是否可以接收CSM阴影 不等于0才可以
static auto* MyCVarMobileEnableStaticAndCSMShadowReceivers = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Mobile.EnableStaticAndCSMShadowReceivers"));
const bool bCombinedStaticAndCSMEnabled = MyCVarMobileEnableStaticAndCSMShadowReceivers->GetValueOnRenderThread()!=0;
// 这个值,开启的时候,被可移动方向光的Primitive,只有当Primitive确定在CSM的范围里的时候,才会通过CSM Shader来进行渲染
static auto* CVarMobileEnableMovableLightCSMShaderCulling = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Mobile.EnableMovableLightCSMShaderCulling"));
const bool bMobileEnableMovableLightCSMShaderCulling = CVarMobileEnableMovableLightCSMShaderCulling->GetValueOnRenderThread() == 1;

// initialize CSMVisibilityInfo for each eligible light.
// 为每一个符合条件的灯光初始化CSMVisibilityInfo
// Scene->MobileDirectionalLights 对于移动端,是每个照明通道中的第一个平行光。
for (FLightSceneInfo* MobileDirectionalLightSceneInfo : Scene->MobileDirectionalLights)
{
const FLightSceneProxy* LightSceneProxy = MobileDirectionalLightSceneInfo ? MobileDirectionalLightSceneInfo->Proxy : nullptr;
if (LightSceneProxy)
{
// UseCSMForDynamicObjects 此灯光是否应仅为动态对象创建CSM
// Primitive可以接收CSM阴影 且 灯光仅为动态对象创建CSM
bool bLightHasCombinedStaticAndCSMEnabled = bCombinedStaticAndCSMEnabled && LightSceneProxy->UseCSMForDynamicObjects();
// CSM开了剔除 且 光源是可移动的 且 需要渲染阴影(ShouldRenderViewIndependentWholeSceneShadows
bool bMovableLightUsingCSM = bMobileEnableMovableLightCSMShaderCulling && LightSceneProxy->IsMovable() && MobileDirectionalLightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows();

// 上面两个满足一个
if (bLightHasCombinedStaticAndCSMEnabled || bMovableLightUsingCSM)
{
// Scene里的图元数量
int32 PrimitiveCount = Scene->Primitives.Num();
// Views 是FSceneRenderer的成员变量 正在渲染的视图
for (auto& View : Views)
{
// TArray<FVisibleLightViewInfo> VisibleLightInfos; 从灯光ID到可见性的映射
// FVisibleLightViewInfo 是 (可见光在其特定的可见视图里的)可见光的信息
// MobileDirectionalLightSceneInfo->Id // int32 Id; // 如果bVisible==true,则这是“Scene->PrimitiveOctree”中primitive的索引。
FMobileCSMSubjectPrimitives& MobileCSMSubjectPrimitives = View.VisibleLightInfos[MobileDirectionalLightSceneInfo->Id].MobileCSMSubjectPrimitives;
// MobileCSMSubjectPrimitives 是FMobileCSMSubjectPrimitives
// FMobileCSMSubjectPrimitives 是 CSM投影体的列表 被移动端渲染器用来剔除(接受静态和CSM阴影的)图元
MobileCSMSubjectPrimitives.InitShadowSubjectPrimitives(PrimitiveCount);
}
}
}
}
// 上面这个循环对场景中的移动端方向光进行遍历,对符合CSM条件的灯光,遍历所有View,初始化每一个view中可见灯光信息里该平行光的CSM投影体的列表,这个列表保存在View的VisibleLightInfos的对应灯光ID的对象的MobileCSMSubjectPrimitives变量里

// 调用公共的初始化动态阴影的函数,这个跳到下面去看
FSceneRenderer::InitDynamicShadows(RHICmdList, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer);

PrepareViewVisibilityLists();

bool bAlwaysUseCSM = false;
for (FLightSceneInfo* MobileDirectionalLightSceneInfo : Scene->MobileDirectionalLights)
{
const FLightSceneProxy* LightSceneProxy = MobileDirectionalLightSceneInfo ? MobileDirectionalLightSceneInfo->Proxy : nullptr;
if (LightSceneProxy)
{
bool bLightHasCombinedStaticAndCSMEnabled = bCombinedStaticAndCSMEnabled && LightSceneProxy->UseCSMForDynamicObjects();
bool bMovableLightUsingCSM = bMobileEnableMovableLightCSMShaderCulling && LightSceneProxy->IsMovable() && MobileDirectionalLightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows();

// non-csm culling movable light will force all draws to use CSM shaders.
// TODO: Cases in which a light channel uses a shadow casting non-csm culled movable light we only really need to use CSM on primitives that match the light channel.
bAlwaysUseCSM = bAlwaysUseCSM || (!bMobileEnableMovableLightCSMShaderCulling && LightSceneProxy->IsMovable() && MobileDirectionalLightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows());
if (bLightHasCombinedStaticAndCSMEnabled || bMovableLightUsingCSM)
{
BuildCSMVisibilityState(MobileDirectionalLightSceneInfo);
}
}
}

for (auto& View : Views)
{
FMobileCSMVisibilityInfo& MobileCSMVisibilityInfo = View.MobileCSMVisibilityInfo;
MobileCSMVisibilityInfo.bAlwaysUseCSM = bAlwaysUseCSM;
}

{
// Check for modulated shadows.
bModulatedShadowsInUse = false;
for (TSparseArray<FLightSceneInfoCompact>::TConstIterator LightIt(Scene->Lights); LightIt && bModulatedShadowsInUse == false; ++LightIt)
{
const FLightSceneInfoCompact& LightSceneInfoCompact = *LightIt;
FLightSceneInfo* LightSceneInfo = LightSceneInfoCompact.LightSceneInfo;
FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos[LightSceneInfo->Id];
// Mobile renderer only projects modulated shadows.
bModulatedShadowsInUse = VisibleLightInfo.ShadowsToProject.Num() > 0;
}
}
}

不同Scene可以共用的初始化动态阴影的部分

FSceneRenderer::InitDynamicShadows

FSceneRenderer::InitDynamicShadows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
void FSceneRenderer::InitDynamicShadows(FRHICommandListImmediate& RHICmdList, FGlobalDynamicIndexBuffer& DynamicIndexBuffer, FGlobalDynamicVertexBuffer& DynamicVertexBuffer, FGlobalDynamicReadBuffer& DynamicReadBuffer)
{
// ... 省略部分源码

// 确认是否为移动端
const bool bMobile = FeatureLevel < ERHIFeatureLevel::SM5;

{
// ... 省略部分源码
for (TSparseArray<FLightSceneInfoCompact>::TConstIterator LightIt(Scene->Lights); LightIt; ++LightIt)
{
// ... 省略部分源码

// Only consider lights that may have shadows.
// 只考虑生成阴影的灯光
if ((LightSceneInfoCompact.bCastStaticShadow || LightSceneInfoCompact.bCastDynamicShadow) && GetShadowQuality() > 0)
{
// see if the light is visible in any view
// 该灯光是不是在某一个view中可见
bool bIsVisibleInAnyView = false;

// ... 省略部分源码
if (bIsVisibleInAnyView)
{
// 可以创建任何一种阴影的话
// 移动端运行时不存在
if (bCreateShadowForMovableLight || bCreateShadowToPreviewStaticLight || bCreateShadowForOverflowStaticShadowing)
{
// Try to create a whole scene projected shadow.
// 试图去创建全场景投影阴影
CreateWholeSceneProjectedShadow(LightSceneInfo, NumPointShadowCachesUpdatedThisFrame, NumSpotShadowCachesUpdatedThisFrame);
}

// Allow movable and stationary lights to create CSM, or static lights that are unbuilt
// 允许可移动光源和定向光源或者是没有build灯光的静态光源创建CSM
// 没有build灯光的静态光源创建是为了预览
if ((!LightSceneInfo->Proxy->HasStaticLighting() && LightSceneInfoCompact.bCastDynamicShadow) || bCreateShadowToPreviewStaticLight)
{
// ...
// 对于移动端来讲 !bMobile肯定是false了
// LightSceneInfo->Proxy->UseCSMForDynamicObjects() 是true,因为要用CSM
// 至少有一个true 可以进入AddViewDependentWholeSceneShadowsForView
if( !bMobile ||
((LightSceneInfo->Proxy->UseCSMForDynamicObjects() || LightSceneInfo->Proxy->IsMovable())
// Mobile uses the scene's MobileDirectionalLights only for whole scene shadows.
// 移动端仅对整个场景阴影使用场景的MobileDirectionalLights
&& (LightSceneInfo == Scene->MobileDirectionalLights[0] || LightSceneInfo == Scene->MobileDirectionalLights[1] || LightSceneInfo == Scene->MobileDirectionalLights[2])))
{
// 跳下去看一下这个
// 收集了CSM相关的数据
AddViewDependentWholeSceneShadowsForView(ViewDependentWholeSceneShadows, ViewDependentWholeSceneShadowsThatNeedCulling, VisibleLightInfo, *LightSceneInfo);
}

// ...
}
}
}
}

// ...
// Calculate visibility of the projected shadows.
InitProjectedShadowVisibility(RHICmdList);
}

// Clear old preshadows and attempt to add new ones to the cache
UpdatePreshadowCache(FSceneRenderTargets::Get(RHICmdList));

// Gathers the list of primitives used to draw various shadow types
GatherShadowPrimitives(PreShadows, ViewDependentWholeSceneShadowsThatNeedCulling, bStaticSceneOnly);

AllocateShadowDepthTargets(RHICmdList);

// Generate mesh element arrays from shadow primitive arrays
GatherShadowDynamicMeshElements(DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer);

}

为当前View添加全场景阴影信息

FSceneRenderer::AddViewDependentWholeSceneShadowsForView
收集CSM相关的数据

  1. 创建了ProjectedShadowInitializer
  2. 用ProjectedShadowInitializer创建了ProjectedShadowInfo
  3. 将ProjectedShadowInfo保存到了InitDynamicShadows函数里的局部变量VisibleLightInfo和ViewDependentWholeSceneShadows中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//
void FSceneRenderer::AddViewDependentWholeSceneShadowsForView(
TArray<FProjectedShadowInfo*, SceneRenderingAllocator>& ShadowInfos,
TArray<FProjectedShadowInfo*, SceneRenderingAllocator>& ShadowInfosThatNeedCulling,
FVisibleLightInfo& VisibleLightInfo,
FLightSceneInfo& LightSceneInfo)
{
// ...

// Allow each view to create a whole scene view dependent shadow
// 允许每一个view去创建一个 whole scene view dependent shadow(CSM是Whole Scene Shadow的一种)
// 我们项目的战斗场景中view数量为1
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
// 取到对应ViewInfo
FViewInfo& View = Views[ViewIndex];

// FLightSceneProxy -> ShadowAmount // 控制阴影遮挡量
// 是1
const float LightShadowAmount = LightSceneInfo.Proxy->GetShadowAmount();
TArray<float, TInlineAllocator<2> > FadeAlphas;
FadeAlphas.Init(0.0f, Views.Num());
// ViewIndex 0 的FadeAlphas 是 1
FadeAlphas[ViewIndex] = LightShadowAmount;

// 在只有一个view的时候下方循环没有意义 暂时跳过不管
// ...

// If rendering in stereo mode we render shadow depths only for the left eye, but project for both eyes!
// 如果是在立体声模式渲染,只对左眼渲染阴影深度但是会对两只眼睛都进行投影 ???
if (IStereoRendering::IsAPrimaryView(View))
{
// 这个在我们移动端一看就是false啊
const bool bExtraDistanceFieldCascade = LightSceneInfo.ShouldRenderLightViewIndependent()
&& LightSceneInfo.Proxy->ShouldCreateRayTracedCascade(View.GetFeatureLevel(), LightSceneInfo.IsPrecomputedLightingValid(), View.MaxShadowCascades);

// 这个那还是1
const int32 ProjectionCount = LightSceneInfo.Proxy->GetNumViewDependentWholeSceneShadows(View, LightSceneInfo.IsPrecomputedLightingValid()) + (bExtraDistanceFieldCascade?1:0);

// ...

FSceneRenderTargets& SceneContext_ConstantsOnly = FSceneRenderTargets::Get_FrameConstantsOnly();


// todo: this code can be simplified by computing all the distances in one place - avoiding some redundant work and complexity
// UE 还没干的事情 通过在一个地方计算所有距离,可以简化此代码 避免一些冗余工作和复杂性
// 看了一眼5.0还是没做 再议
for (int32 Index = 0; Index < ProjectionCount; Index++)
{
// 声明了一个WholeSceneProjectedShadow的初始化器
FWholeSceneProjectedShadowInitializer ProjectedShadowInitializer;

// 0
int32 LocalIndex = Index;

// Indexing like this puts the ray traced shadow cascade last (might not be needed)
// bExtraDistanceFieldCascade是false
if(bExtraDistanceFieldCascade && LocalIndex + 1 == ProjectionCount)
{
LocalIndex = INDEX_NONE;
}

// 通过GetViewDependentWholeSceneProjectedShadowInitializer去填充WholeSceneProjectedShadow的初始化器的相关成员
// 跳到下面看FWholeSceneProjectedShadowInitializer是什么
// 跳到下面看GetViewDependentWholeSceneProjectedShadowInitializer干了什么
// 简单总结一下这个函数里都干了什么:
// 1. 通过视口摄像机的远近平面的八个点创建了一个包围球
// 2. 填充了ProjectedShadowInitializer的各项属性,其中有些值和1里的包围球的值有关
if (LightSceneInfo.Proxy->GetViewDependentWholeSceneProjectedShadowInitializer(View, LocalIndex, LightSceneInfo.IsPrecomputedLightingValid(), ProjectedShadowInitializer))
{
// ShadowBuffer分辨率
const FIntPoint ShadowBufferResolution(
FMath::Clamp(GetCachedScalabilityCVars().MaxCSMShadowResolution, 1, (int32)GMaxShadowDepthBufferSizeX),
FMath::Clamp(GetCachedScalabilityCVars().MaxCSMShadowResolution, 1, (int32)GMaxShadowDepthBufferSizeY));

// Create the projected shadow info.
// 创建投影阴影信息对象
FProjectedShadowInfo* ProjectedShadowInfo = new(FMemStack::Get(), 1, 16) FProjectedShadowInfo;

// ES31小于 是0
uint32 ShadowBorder = NeedsUnatlasedCSMDepthsWorkaround(FeatureLevel) ? 0 : SHADOW_BORDER;

// 给ProjectedShadowInfo对象填充信息
ProjectedShadowInfo->SetupWholeSceneProjection(
&LightSceneInfo,
&View,
ProjectedShadowInitializer,
ShadowBufferResolution.X - ShadowBorder * 2,
ShadowBufferResolution.Y - ShadowBorder * 2,
ShadowBorder,
false // no RSM
);

// 是1
ProjectedShadowInfo->FadeAlphas = FadeAlphas;
// 填充完了ProjectedShadowInfo的各种信息

FVisibleLightInfo& LightViewInfo = VisibleLightInfos[LightSceneInfo.Id];
// 将填充完了的投影阴影信息添加到了这几个地方
VisibleLightInfo.MemStackProjectedShadows.Add(ProjectedShadowInfo);
VisibleLightInfo.AllProjectedShadows.Add(ProjectedShadowInfo);
ShadowInfos.Add(ProjectedShadowInfo);

// ...
}
}

// 下面这部分在移动端相当于没有,暂时略过
FSceneViewState* ViewState = (FSceneViewState*)View.State;
if (ViewState)
{
// 移动端ES是NULL
FLightPropagationVolume* LightPropagationVolume = ViewState->GetLightPropagationVolume(View.GetFeatureLevel());

float LPVIntensity = 0.f;

// ...

// ...
}
}
}
}

设置投影阴影初始化器

  1. 就是那个每个级联的SplitNear和SplitFar,就是分割级联,这个是在计算包围球的时候才计算的,具体计算方法是GetSplitDistance
  2. 计算了每个级联的外界包围球,具体方法是GetShadowSplitBounds
  3. 计算了这个球的内接盒
  4. 在计算包围球的同时计算了阴影裁剪体

GetViewDependentWholeSceneProjectedShadowInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* Sets up a projected shadow initializer that's dependent on the current view for shadows from the entire scene.
设置投影阴影初始化器,该初始化器依赖于整个场景中阴影的当前视图。
* @param InCascadeIndex ShadowSplitIndex or INDEX_NONE for the the DistanceFieldCascade
* @return True if the whole-scene projected shadow should be used.
如果应使用整个场景投影阴影,则返回True。
*/
virtual bool GetViewDependentWholeSceneProjectedShadowInitializer(const FSceneView& View, int32 InCascadeIndex, bool bPrecomputedLightingIsValid, FWholeSceneProjectedShadowInitializer& OutInitializer) const override
{
// false
const bool bRayTracedCascade = (InCascadeIndex == INDEX_NONE);

// 根据摄像机的远近平面的八个点生成了一个包围球
FSphere Bounds = FDirectionalLightSceneProxy::GetShadowSplitBounds(View, InCascadeIndex, bPrecomputedLightingIsValid, &OutInitializer.CascadeSettings);

// 还没具体看
// 简单看了一下这个函数的返回值 return FMath::Min<int32>(NumCascades, MaxShadowCascades);
// MaxShadowCascades是View.MaxShadowCascades 要渲染的最大阴影级联数
//
uint32 NumNearCascades = GetNumShadowMappedCascades(View.MaxShadowCascades, bPrecomputedLightingIsValid);

// Last cascade is the ray traced shadow, if enabled
// ShadowSplitIndex是级联阴影分割视锥的索引,是传进来的参数,如果是ray traced shadow的话必须是上面获得的NumNearCascades,就是得是最后一个
OutInitializer.CascadeSettings.ShadowSplitIndex = bRayTracedCascade ? NumNearCascades : InCascadeIndex;

// 虽然不知道为啥,用前面算出来的球的半径除以根号3
// 是Box边长的一半,是神奇的计算方法
const float ShadowExtent = Bounds.W / FMath::Sqrt(3.0f);

// 这里定义了一个FBoxSphereBounds的对象
// FBoxSphereBounds就是保存了包围球和包围盒的属性信息,原点,上面算出来的Extent,以及球的半径
// 往后看了一下- -这里这个FBoxSphereBounds就是存了一下算出来的包围球的Origin、半径、半径除以根号3的值组成的FVector,之后用到这三个值的地方都从这个对象取值
const FBoxSphereBounds SubjectBounds(Bounds.Center, FVector(ShadowExtent, ShadowExtent, ShadowExtent), Bounds.W);

// FVector
// A translation that is applied to world-space before transforming by one of the shadow matrices.
// 无情机翻:在通过一个阴影矩阵进行变换之前应用于世界空间的变换。
// 通过一个阴影矩阵进行变化之前,被应用于世界空间的,一个,translation?平移
// 为啥把包围球的中心点取了个反?
OutInitializer.PreShadowTranslation = -Bounds.Center;

// FMatrix
// 世界空间转光照空间的矩阵
// 等价于灯光旋转矩阵的逆矩阵
OutInitializer.WorldToLight = FInverseRotationMatrix(GetDirection().GetSafeNormal().Rotation());

// FVector
// Non-uniform scale to be applied after WorldToLight.
// 无情机翻:WorldToLight之后应用的非均匀比例
OutInitializer.Scales = FVector(1.0f,1.0f / Bounds.W,1.0f / Bounds.W);

// FVector
// 注释都没有!
OutInitializer.FaceDirection = FVector(1,0,0);

// FBoxSphereBounds
// 连个注释都没有!
// 原点改成0了
OutInitializer.SubjectBounds = FBoxSphereBounds(FVector::ZeroVector, SubjectBounds.BoxExtent, SubjectBounds.SphereRadius);

// FVector4
// 连个注释都没有!
OutInitializer.WAxis = FVector4(0,0,0,1);

// float
// 连个注释都没有!
// Use the minimum of half the world, things further away do not cast shadows,
// However, if the cascade bounds are larger, then extend the casting distance far enough to encompass the cascade.
// 使用半个世界的最小值,更远的物体不会投射阴影,但是,如果层叠边界更大,则将投射距离延伸到足以覆盖层叠的程度。
// #define WORLD_MAX 2097152.0 /* Maximum size of the world */
// #define HALF_WORLD_MAX (WORLD_MAX * 0.5) /* Half the maximum size of the world */
// 包围球半径的负值和世界最小尺寸的一半,两者取较小的,就是不能比世界最大尺寸的一半的负值还要小,那就不在世界内了
OutInitializer.MinLightW = FMath::Min<float>(-HALF_WORLD_MAX, -SubjectBounds.SphereRadius);

// Range must extend to end of cascade bounds
// 范围必须扩展到级联边界的末尾
const float MaxLightW = SubjectBounds.SphereRadius;

// float
// 连个注释都没有!
// MinLightW是负的包围球半径,MaxLightW是正的包围球半径,就是求出来一个包围球直径
// 如果球的半径大于了世界最大尺寸的一半的时候,这个MaxDistanceToCastInLightW是比世界尺寸要大的?
OutInitializer.MaxDistanceToCastInLightW = MaxLightW - OutInitializer.MinLightW;
// 那移动端肯定是false啊
OutInitializer.bRayTracedDistanceField = bRayTracedCascade;
// When enabled, the cascade only renders objects marked with bCastFarShadows enabled (e.g. Landscape).
// 启用时,级联仅渲染标记为已启用bCastFarShadows的对象(例如,Landscape)。
OutInitializer.CascadeSettings.bFarShadowCascade = !bRayTracedCascade && OutInitializer.CascadeSettings.ShadowSplitIndex >= (int32)NumNearCascades;
return true;
}

生成投影阴影的信息

SetupWholeSceneProjection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// ShadowSetup.cpp
void FProjectedShadowInfo::SetupWholeSceneProjection(
FLightSceneInfo* InLightSceneInfo,
FViewInfo* InDependentView,
const FWholeSceneProjectedShadowInitializer& Initializer,
uint32 InResolutionX,
uint32 InResolutionY,
uint32 InBorderSize,
bool bInReflectiveShadowMap)
{
// 从这里开始进行了一系列的变量赋值
// FLightSceneInfo
LightSceneInfo = InLightSceneInfo;
// FLightSceneInfoCompact
LightSceneInfoCompact = InLightSceneInfo;
// FViewInfo
DependentView = InDependentView;
// FVector
PreShadowTranslation = Initializer.PreShadowTranslation;
// FShadowCascadeSettings
CascadeSettings = Initializer.CascadeSettings;
//
ResolutionX = InResolutionX;
ResolutionY = InResolutionY;
//
bDirectionalLight = InLightSceneInfo->Proxy->GetLightType() == LightType_Directional;
bOnePassPointLightShadow = Initializer.bOnePassPointLightShadow;
bRayTracedDistanceField = Initializer.bRayTracedDistanceField;
bWholeSceneShadow = true;
bTransmission = InLightSceneInfo->Proxy->Transmission();
bHairStrandsDeepShadow = InLightSceneInfo->Proxy->CastsHairStrandsDeepShadow();
// mobile 所以 false
bReflectiveShadowmap = bInReflectiveShadowMap;
// 居然是0耶
BorderSize = InBorderSize;

FVector XAxis, YAxis;
Initializer.FaceDirection.FindBestAxisVectors(XAxis,YAxis);
// 从世界空间转换到灯光视图空间的矩阵
// 都是在FWholeSceneProjectedShadowInitializer中计算过的
const FMatrix WorldToLightScaled = Initializer.WorldToLight * FScaleMatrix(Initializer.Scales);
const FMatrix WorldToFace = WorldToLightScaled * FBasisVectorMatrix(-XAxis,YAxis,Initializer.FaceDirection.GetSafeNormal(),FVector::ZeroVector);

MaxSubjectZ = WorldToFace.TransformPosition(Initializer.SubjectBounds.Origin).Z + Initializer.SubjectBounds.SphereRadius;
MinSubjectZ = FMath::Max(MaxSubjectZ - Initializer.SubjectBounds.SphereRadius * 2,Initializer.MinLightW);

// 这次看的是从Mobile管线过来的 这个是false 没有RSM
if(bInReflectiveShadowMap)
{
// 因为mobile没有RSM我把这里跳过啦!
}
else
{
// 是方向光!
if(bDirectionalLight)
{
// Limit how small the depth range can be for smaller cascades
// 对于比较小的级联,限制最小的depth,不能太小
// This is needed for shadow modes like subsurface shadows which need depth information outside of the smaller cascade depth range
// 对于某些阴影模式是必须要的(比如 subsurface阴影(需要在很小的级联depth范围之外的depth信息),这又是啥?
//@todo - expose this value to the ini
// 是一个限制范围数字,为什么是5000 不重要
const float DepthRangeClamp = 5000;
// 最大值不能小于5000
MaxSubjectZ = FMath::Max(MaxSubjectZ, DepthRangeClamp);
// 最小值不能小于-5000
MinSubjectZ = FMath::Min(MinSubjectZ, -DepthRangeClamp);

// Transform the shadow's position into shadowmap space
// 将阴影的位置变换为shadowmap空间
// 这个PreShadowTranslation是计算出的包围球的origin取反,这里再次取反,这不又回去了吗
const FVector TransformedPosition = WorldToFace.TransformPosition(-PreShadowTranslation);

// Largest amount that the shadowmap will be downsampled to during sampling
// shadowmap将会被降采样到所允许的最大数量
// We need to take this into account when snapping to get a stable result
//
// This corresponds to the maximum kernel filter size used by subsurface shadows in ShadowProjectionPixelShader.usf
const int32 MaxDownsampleFactor = 4;
// Determine the distance necessary to snap the shadow's position to the nearest texel
// 确定将阴影位置捕捉到最近的纹理所需的距离
const float SnapX = FMath::Fmod(TransformedPosition.X, 2.0f * MaxDownsampleFactor / InResolutionX);
const float SnapY = FMath::Fmod(TransformedPosition.Y, 2.0f * MaxDownsampleFactor / InResolutionY);
// Snap the shadow's position and transform it back into world space
// This snapping prevents sub-texel camera movements which removes view dependent aliasing from the final shadow result
// This only maintains stable shadows under camera translation and rotation
// 捕捉阴影的位置并将其转换回世界空间
// 此捕捉可防止sub-texel摄影机移动,从而从最终阴影结果中移除与视图相关的锯齿
// 这只会在摄影机平移和旋转下保持稳定的阴影
// 意思是为了避免摄像机移动的造成的锯齿或者抖动,重新计算了PreShadowTranslation
const FVector SnappedWorldPosition = WorldToFace.InverseFast().TransformPosition(TransformedPosition - FVector(SnapX, SnapY, 0.0f));

//上面做的就是 按照某种规则重新计算了PreShadowTranslation,也就是包围球的origin,通过这个计算可以避免摄像机移动造成的锯齿和抖动
PreShadowTranslation = -SnappedWorldPosition;
}

if (CascadeSettings.ShadowSplitIndex >= 0 && bDirectionalLight)
{
checkSlow(InDependentView);
// 计算FProjectedShadowInfo的ShadowBound
ShadowBounds = InLightSceneInfo->Proxy->GetShadowSplitBounds(
*InDependentView,
bRayTracedDistanceField ? INDEX_NONE : CascadeSettings.ShadowSplitIndex,
InLightSceneInfo->IsPrecomputedLightingValid(),
0);
}
else
{
ShadowBounds = FSphere(-Initializer.PreShadowTranslation, Initializer.SubjectBounds.SphereRadius);
}

// Any meshes between the light and the subject can cast shadows, also any meshes inside the subject region
// 灯光和对象之间的任何网格都可以投射阴影,也可以投射对象区域内的任何网格
// CasterMatrix??是什么??
// 阴影投射矩阵,肯定是用来处理变换的,但是不知道是处理什么变换
const FMatrix CasterMatrix = WorldToFace * FShadowProjectionMatrix(Initializer.MinLightW, MaxSubjectZ, Initializer.WAxis);
// 获取投射阴影的视锥体
// Frustum containing all potential shadow casters.
// 包含所有潜在阴影投射者的平截头体,是FProjectedShadowInfo的成员变量
// 根据函数的参数变量名,const FMatrix& ViewProjectionMatrix,是一个视图投影矩阵
// 该函数通过CasterMatrix,生成了CasterFrustum的六个Planes
GetViewFrustumBounds(CasterFrustum, CasterMatrix, true);
}

checkf(MaxSubjectZ > MinSubjectZ, TEXT("MaxSubjectZ %f MinSubjectZ %f SubjectBounds.SphereRadius %f"), MaxSubjectZ, MinSubjectZ, Initializer.SubjectBounds.SphereRadius);

MinPreSubjectZ = Initializer.MinLightW;

SubjectAndReceiverMatrix = WorldToFace * FShadowProjectionMatrix(MinSubjectZ, MaxSubjectZ, Initializer.WAxis);
// For CSM the subject is the same as the receiver (i.e., the cascade bounds)
ReceiverMatrix = SubjectAndReceiverMatrix;

float MaxSubjectDepth = SubjectAndReceiverMatrix.TransformPosition(
Initializer.SubjectBounds.Origin
+ WorldToLightScaled.InverseFast().TransformVector(Initializer.FaceDirection) * Initializer.SubjectBounds.SphereRadius
).Z;

if (bOnePassPointLightShadow)
{
MaxSubjectDepth = Initializer.SubjectBounds.SphereRadius;
}

InvMaxSubjectDepth = 1.0f / MaxSubjectDepth;

// Store the view matrix
// Reorder the vectors to match the main view, since ShadowViewMatrix will be used to override the main view's view matrix during shadow depth rendering
ShadowViewMatrix = Initializer.WorldToLight *
FMatrix(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, 0, 1));

InvReceiverMatrix = ReceiverMatrix.InverseFast();

// ReceiverFrustum 接收阴影的视锥体
GetViewFrustumBounds(ReceiverFrustum, ReceiverMatrix, true);

UpdateShaderDepthBias();
}

相关结构体

FWholeSceneProjectedShadowInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Information needed to create a whole scene projected shadow. */
// 创建整个场景投影阴影所需的信息
class ENGINE_API FWholeSceneProjectedShadowInitializer : public FProjectedShadowInitializer
{
public:
FShadowCascadeSettings CascadeSettings;
bool bOnePassPointLightShadow;
bool bRayTracedDistanceField;

FWholeSceneProjectedShadowInitializer() :
bOnePassPointLightShadow(false),
bRayTracedDistanceField(false)
{}

bool IsCachedShadowValid(const FWholeSceneProjectedShadowInitializer& CachedShadow) const
{
return FProjectedShadowInitializer::IsCachedShadowValid((const FProjectedShadowInitializer&)CachedShadow)
&& bOnePassPointLightShadow == CachedShadow.bOnePassPointLightShadow
&& bRayTracedDistanceField == CachedShadow.bRayTracedDistanceField;
}
};

FProjectedShadowInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** A projected shadow transform. */
class ENGINE_API FProjectedShadowInitializer
{
public:

/** A translation that is applied to world-space before transforming by one of the shadow matrices. */
// 通过一个阴影矩阵进行变化之前的,被应用于世界空间的,一个,translation ?平移
FVector PreShadowTranslation;

FMatrix WorldToLight;
/** Non-uniform scale to be applied after WorldToLight. */
FVector Scales;

FVector FaceDirection;
FBoxSphereBounds SubjectBounds;
FVector4 WAxis;
float MinLightW;
float MaxDistanceToCastInLightW;

/** Default constructor. */
FProjectedShadowInitializer()
{}

bool IsCachedShadowValid(const FProjectedShadowInitializer& CachedShadow) const
{
return PreShadowTranslation == CachedShadow.PreShadowTranslation
&& WorldToLight == CachedShadow.WorldToLight
&& Scales == CachedShadow.Scales
&& FaceDirection == CachedShadow.FaceDirection
&& SubjectBounds.Origin == CachedShadow.SubjectBounds.Origin
&& SubjectBounds.BoxExtent == CachedShadow.SubjectBounds.BoxExtent
&& SubjectBounds.SphereRadius == CachedShadow.SubjectBounds.SphereRadius
&& WAxis == CachedShadow.WAxis
&& MinLightW == CachedShadow.MinLightW
&& MaxDistanceToCastInLightW == CachedShadow.MaxDistanceToCastInLightW;
}
};

FBoxSphereBounds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Structure for a combined axis aligned bounding box and bounding sphere with the same origin. (28 bytes).
具有相同原点的组合轴对包围盒和包围球的结构。(28字节)。????
*/
struct FBoxSphereBounds
{
/** Holds the origin of the bounding box and sphere. */
// 包围盒和球的原点
FVector Origin;
// 包围盒的的边长的一半
/** Holds the extent of the bounding box. */
FVector BoxExtent;

/** Holds the radius of the bounding sphere. */
// 包围球的半径
float SphereRadius;
}

UE4:移动端级联阴影简要分析
http://muchenhen.com/posts/46173/
作者
木尘痕
发布于
2022年6月16日
许可协议