虚幻4移动端级联阴影ShadowMap使用率过低问题修复
在微软家的这篇 [Common Techniques to Improve Shadow Depth Maps]文章中,详细讨论Shadow Map的各种问题和解决方案,里面的摩尔纹、Peter Pan、抗锯齿等常规问题都有着详细的解决方案。在简述Shadow Camera Setup的时候,他们简单提了下Shadow Camera Fit To Scene和Fit To View的两种做法,而这两种做法区别,对ShadowMap的利用率影响非常的大。
UE4内部的阴影解决方案很多,不过移动端能用上的就只有CSM。为了兼顾移动端的性能表现,设置CSM的数目 = 1就够用了,这时候的CSM实际上和单纯的Project ShadowMap算法没啥太大区别了,但是CSM数目设置为1时,UE4的阴影显得非常的淡,在人物角色上阴影表现得非常的轻浮,没有色彩感官上体现的厚重感,层次感也没体现出来,大场景时感觉还好。在人物特写的角色展示界面,这阴影质量根本不够用,emm,我记得我第一天搬下来游缘的时候滔哥就跟我说UE的阴影有问题,不过那时啥都不会天天划水(当然现在也啥都不会天天划水),过了好几个月都不想动这个阴影,直到最近几天有空了于是安排时间来解决这个问题。
国庆放假回来后就开始着手解决这些问题,修改这部分的源码,一共花了大概四天时间来看引擎的CSM部分的源码,我一开始的重心是放在了阴影相机的投影矩阵构建函数上,在那里折腾了好久吧,实际上也没啥收获,不过后来在微软家的DX12 Samples里发现了Mesh Viewer实例工程,里面的Sponza阴影贴图蛮好的。而微软他们家构建投影矩阵的方式和UE4也一样,唯一不同的就是相机视锥包围盒的构建方式不同,微软他们家是写死了的。UE4使用了Fit To View的方式。
UE4级联阴影Project Matrix的Setup步骤如下:
- 根据编辑器中的CSM设置计算CSM每一级的范围距离等信息。GetShadowSplitBounds
- 计算当前级联的视锥体包围球。GetShadowSplitBoundsDepthRange
- 根据当前包围球范围剔除包围球外的投影体。
- 根据包围球设置阴影相机的投影矩阵。
在上面四个步骤中,有意思的地方是步骤二的包围球计算。他们先计算了视锥的八个顶点,然后根据这八个顶点计算一个包围球。算法如下:
// Calculate the ideal bounding sphere for the subfrustum.
// Fx = (Db^2 - da^2) / 2Fl + Fl / 2
// (where Da is the far diagonal, and Db is the near, and Fl is the frustum length)
float OptimalOffset = (DiagonalBSq - DiagonalASq) / (2.0f * FrustumLength) + FrustumLength * 0.5f;
float CentreZ = SplitFar - OptimalOffset;
CentreZ = FMath::Clamp( CentreZ, SplitNear, SplitFar );
FSphere CascadeSphere(ViewOrigin + CameraDirection * CentreZ, 0);
for (int32 Index = 0; Index < 8; Index++)
{
CascadeSphere.W = FMath::Max(CascadeSphere.W, FVector::DistSquared(CascadeFrustumVerts[Index], CascadeSphere.Center));
}
这种做法比直接根据八个顶点求中点然后取最大半径(一把梭的方式,我的最爱)的构建方式得到的包围球半径要小一点。
Emmm,最终,问题找到了,我接下来着手把UE4的阴影相机Bounds构建方式改成了Fit To View Scene的方式(这是什么鬼方式?)。命名有点奇怪,因为我既按照Fit To View的方式构建了当前级联的视锥体,再在当前级联视锥体内构建所有Shadow Caster的Combine Bounds,最终根据原来的视锥体Bounds和后来的Shadow Caster Combine Bounds做半径比较,返回半径小的那个,这样,在投影体少的场景中,既可以利用Fit To Scene充分切合投影的优势,又可以在投影体多的场景中利用Fit To View的高利用率优势。
我这里的修改实际上把 Fit To View 和 Fit To Scene都算了一遍,性能有点吃不消,profile时显示在骁龙660上每帧多花了0.12ms的cpu时间。
然后15、16号项目组准备搬家,这两天也是无心工作。16号下午美术部这边就剩下我一个人了,临近下班的时候正好引擎编译通过了,在RenderDoc里面测试了一切正常,现在Shadow Map利用率非常的高。
另外,当场景中投影体Bounds小,但是距离离得比较远时,原始的Combine操作导致Bounds增长速度非常的快,而远处的Caster实际上并不重要,加入一个根据相机视点距离和Caster Bounds大小作为权重的有偏Combine可以有效解决这个问题。
最后,对于选角、角色展示等场景,Caster很少时,我直接根据整个场景的Caster bounds来构建投影矩阵,连视锥构建都省了,快捷方便。XD。