UE5.8 大世界光照与阴影策略:太阳主光到山谷远景的阴影距离账
引子:阿杰的山谷阴影
阿杰是我带的渲染组主美,上个月被阴影整得够呛。他负责的新开放世界关卡是一片高山峡谷,玩家在悬崖边能俯瞰到两公里外的营地。场景美术把远处的瞭望塔、帐篷和岩石都摆好了,结果在 RTX 4080 上跑起来,帧率直接从 60 掉到 38。
一开始大家都怀疑是 Lumen 全局光照太烧钱。我让他用 RenderDoc 抓了一帧,发现 GPU 时间大头在 Shadow Depths。太阳主光的阴影贴图占了 4.2 毫秒,远处那些只有几个像素大的帐篷还在认认真真地画阴影。阿杰把阴影距离从 10000 调到 5000,帧率回来了,但远处地面变成了一块没有起伏的平底,营地的轮廓也糊成一片。
那天晚上我们坐在会议室抽烟,他说了一句话我印象很深:「大世界的光照不是亮不亮的问题,是阴影要画多远、画多细的问题。」
这篇文章就围绕这句话,把 UE5.8 里大世界光照与阴影的关键策略摊开来讲。
一、远距离阴影:画到地平线是一笔昂贵的账
大世界的视觉可信度很大程度靠远距离阴影撑着。玩家站在高处,远处山脉的沟壑、森林的轮廓、建筑的剪影,全是阴影在讲故事。但要把阴影一直画到地平线,直接拉高 Shadow Distance 是最 naive 的做法,也是最容易爆炸的做法。
UE5.8 沿用了 CSM(Cascaded Shadow Maps)作为 Directional Light 的默认阴影方案。它的思路是把视锥从近到远切成几段,每段用一张独立的分辨率阴影贴图。近处贴图分辨率高,远处低。参数里能看到的 Dynamic Shadow Distance Movable Lights、Num Dynamic Shadow Cascades、Cascade Distribution Exponent 就是在控制这件事。
听起来很合理,但问题在于大世界里视锥很长。如果玩家站在山顶,远处营地到摄像机的距离可能是 8000 厘米甚至更远。CSM 的级联总数通常只给 3 到 4 级,每一级都要覆盖比上一级大得多的范围。想让最后一级覆盖到地平线,那最后一级的 texel 在世界空间里会大得惊人,阴影边缘全是锯齿。
阿杰最初就是把 Dynamic Shadow Distance 拉到 20000,Num Cascades 拉到 4,结果 4096 分辨率的阴影贴图摊到那么大范围,单个 texel 对应世界空间几十厘米。远处岩石的阴影看起来像打了马赛克。
更隐蔽的问题是 GPU 开销。CSM 每一级都是一次完整的 shadow pass,级数增加、距离拉长,draw call 和像素填充都会线性上涨。大世界场景里 Static Mesh 本来就多,阴影 pass 里还得再画一遍。我们在那个峡谷关卡里测过,Dynamic Shadow Distance 从 8000 提到 15000,Shadow Depths 时间从 2.1ms 涨到 4.8ms。
所以远距离阴影的核心问题变成了:哪些阴影必须由动态 CSM 承担,哪些可以交给静态烘焙或专门的远距离阴影技术。
二、CSM 配置:级联不是越多越好
CSM 的参数调不好,大世界会特别难看。UE5.8 的 Directional Light Details 面板里跟 CSM 相关的主要参数有这几个:
- Dynamic Shadow Distance Movable Lights
- Num Dynamic Shadow Cascades
- Cascade Distribution Exponent
- Cascade Transition Fraction
- Shadow Distance
- Shadow Bias
Dynamic Shadow Distance 决定动态阴影从摄像机开始画多远。它只影响 Movable Light,Static Light 的烘焙阴影另算。Num Dynamic Shadow Cascades 是级联数量,常见取值 2 到 4。Cascade Distribution Exponent 控制级联如何按距离分布,值越大,近处级联占的距离比例越大,远处级联被压缩。
这个 Exponent 很多团队一上来就调得很高,想把分辨率尽量留给近处。但大世界里有大量中距离物体,比如山坡上的一片树林、河流对岸的废墟,如果 Exponent 太大,第二级和第三级之间的过渡会很陡,玩家移动时阴影会突然变糊。
阿杰后来试了一组参数:Dynamic Shadow Distance 8000,Num Cascades 3,Cascade Distribution Exponent 3.0,Cascade Transition Fraction 0.1。中距离树林的阴影稳定了,但远处地平线还是空白。
这时就需要补一套远距离阴影方案。UE 提供了几种选择:
- Distance Field Shadows(DF Shadow):基于 SDF 的阴影,适合远处静态物体,开销低但精度有限。
- Virtual Shadow Maps(VSM):UE5 主推的高精度阴影系统,基于虚拟纹理页分配。
- 烘焙静态阴影:Lightmass 或 GPU Lightmass 烘焙到 lightmap 里的静态阴影。
- 手动做远景阴影贴图:把远处地形和建筑投影到一张 low-res 阴影图,挂在天空球或地形材质里。
这几种方案没有哪一种是万金油,通常要组合使用。阿杰的关卡最终是 CSM 管 0 到 8000 厘米的动态近中距离阴影,DF Shadow 管 8000 到 30000 厘米的远处静态物体,更远的地平线靠烘焙 lightmap 和地形法线细节撑着。
下面这张图是我们当时的分层思路。
flowchart TD
subgraph camera ["摄像机视锥"]
near["0 - 2000 cm"]
mid["2000 - 8000 cm"]
far["8000 - 30000 cm"]
horizon["> 30000 cm"]
end
subgraph shadow_tech ["阴影技术"]
csm["CSM 动态级联"]
dfs["Distance Field Shadows"]
bake["烘焙静态阴影"]
fake["远景 fake shadow"]
end
subgraph output ["画面效果"]
sharp["锐利近影"]
soft["柔和中远影"]
horizon_look["地平线剪影"]
end
near --> csm
mid --> csm
far --> dfs
horizon --> bake
horizon --> fake
csm --> sharp
csm --> soft
dfs --> soft
bake --> horizon_look
fake --> horizon_look这种分层的好处是每段阴影都用最合适的技术做,而不是让 CSM 一个人扛所有距离。缺点是美术和 TA 需要维护多套系统,调起来比较繁琐。
三、Virtual Shadow Maps:大世界的救星还是新坑
UE5 把 Virtual Shadow Maps 当成次世代阴影方案来推。它用虚拟纹理页的思想,只在屏幕上可见的区域分配高分辨率 shadow page,理论上可以做到近处极高精度、远处按需降级。UE5.8 对 VSM 的完善度已经比 5.0 时期高很多,崩溃和漏阴影的情况少了很多。
VSM 的核心优势是精度。传统 CSM 的阴影分辨率是全局固定的,比如 4096 贴图摊到整个级联范围,每个 texel 在屏幕上的大小不均匀。VSM 把阴影空间划分成很多页,只给屏幕上看得到、又需要高精度的区域分配高分辨率页。近处一个角色脚下的阴影可以占用好几页,远处地平线可能只占用一页。
我们做过一个测试:在同一个峡谷场景里,CSM 4 级联 4096 分辨率,和 VSM 默认配置对比。近处岩石的阴影 VSM 明显更锐利,边缘锯齿少;中距离树林的接触阴影也更稳定。GPU 时间上两者其实差不多,VSM 略低一点,但 VSM 的内存占用更高,因为它要维护页表和缓存。
但 VSM 在大世界里也有几个实实在在的坑。
第一个坑是半透明和毛发。VSM 对不透明物体效果最好,半透明物体、头发、草地的阴影还是容易出问题。我们在一片高草地区域发现草叶阴影会闪烁,最后只能把那片草的 Cast Shadow 关掉,改用地面 fake shadow。
第二个坑是 Nanite 与 VSM 的协同。Nanite 的三角形极细,VSM 需要足够高的页分辨率才能避免漏阴影。如果项目里 Nanite 用得特别激进,VSM 的 page cache 压力会变大。阿杰在山谷里放了几块 Nanite 岩石,结果移动相机时阴影偶尔会出现一帧的黑块,后来把 VSM Page Pool Size 从默认 4096 提到 8192 才缓解。
第三个坑是远距离。VSM 虽然理论上能覆盖很远,但 page 分配仍然受屏幕空间驱动。地平线上一座小塔在屏幕上只占几个像素,VSM 不会给它分配多少分辨率,所以远处阴影仍然是糊的。大世界不能指望 VSM 单独解决远距离问题,它更适合中近距离的高品质阴影。
这里有一个关键公式可以帮助理解 VSM 的精度分配。设屏幕空间中某个物体占据的像素数为 ,VSM 为其分配的 shadow page 像素数为 ,则有效阴影采样密度可以近似为:
其中 是光线方向与表面法线的夹角。当 接近 90 度,也就是接近掠射光时, 趋近于 0,有效采样密度会急剧下降,阴影质量变差。这也是为什么日落时分的阴影总是比正午更难处理。
在配置里,VSM 有几个关键参数:
- r.Shadow.Virtual.Enable:总开关,设为 1 开启。
- r.Shadow.Virtual.PagePoolSizeKB:页池大小,单位 KB。大世界建议 8192 或更高。
- r.Shadow.Virtual.NonNanite.VSM:是否允许非 Nanite 物体使用 VSM。
- r.Shadow.Virtual.SMRT.RayCountLocal / RayCountDirectional:SMRT 软阴影采样数,数值越高越柔和也越贵。
如果需要在运行时按场景动态调整 Directional Light 的阴影距离,可以在关卡蓝图或 C++ 里拿到 UDirectionalLightComponent 然后改 CascadeDistributionExponent 和 DynamicShadowDistanceMovableLight。下面是一段示例:
void UMyLightingManager::SetWorldShadowDistance(float Distance)
{
if (!DirectionalLight || !DirectionalLight->GetLightComponent())
{
return;
}
UDirectionalLightComponent* LightComp = DirectionalLight->GetLightComponent();
LightComp->SetDynamicShadowDistanceMovableLight(Distance);
LightComp->CascadeDistributionExponent = 3.0f;
LightComp->NumDynamicShadowCascades = 3;
LightComp->MarkRenderStateDirty();
}这段代码适合在玩家进入不同地貌区域时调用,比如从森林切到平原就拉远距离,进城就缩短。
阿杰最终的 VSM 配置是 Page Pool 8192KB,SMRT 用默认 8 根 ray,只给 Directional Light 和关键 Spot Light 开 VSM,点光源保留传统阴影贴图。这样整体上 GPU 时间比纯 CSM 少 0.3ms,但内存多了 120MB 左右。
四、Lightmass vs Lumen:烘焙不是过时技术
UE5 主推 Lumen 之后,很多团队直接把 Lightmass 扔了。大世界项目里,我觉得这个做法有点激进。Lumen 确实香,动态 GI、反射、天空光都能实时更新,但它在超远距离和复杂静态场景里的稳定性不如烘焙。
Lumen 的基本原理是用 Screen Space Probe 和 Mesh Distance Field 做间接光追踪。近处靠屏幕空间,中远处靠 SDF。它的开销主要来自 tracing 和 irradiance cache 更新。大世界的问题是 SDF 分辨率有限,远处建筑的 GI 细节会被平滑掉,而且 Lumen 对大面积开放天空地面的处理比较吃力。
Lightmass 或者 GPU Lightmass 的好处是结果稳定、质量高、运行时开销低。烘焙好的 lightmap 在运行时就是一张纹理采样,几乎不花 GPU 时间。大世界里大量不会动的地形、岩石、建筑,非常适合烘焙静态光照和静态阴影。
但 Lightmass 的代价是反复调整很慢。改一个灯光角度要重新烘焙,大世界关卡烘焙一次可能几十分钟甚至几个小时。而且动态物体没法接收高质量的静态间接光,需要靠 Light Propagation Volume 或 Lumen 来补。
我们的做法是分区混合:
- 地形和大型静态岩石:GPU Lightmass 烘焙 lightmap,提供稳定的基底间接光和远距离阴影。
- 中近距离建筑和道具:Static Light 烘焙 + Movable Light 动态阴影叠加。
- 角色和可互动物体:Lumen 动态 GI + CSM/VSM 动态阴影。
这种混合方案比全 Lumen 稳,也比全烘焙灵活。阿杰的山谷里,远山和谷底的大片岩石都是烘焙的,玩家脚下的动态阴影由太阳 CSM 提供,角色身上的间接光由 Lumen 计算。最终 GPU 时间比全 Lumen 少了 1.8ms,视觉差别不大。
下面这张图说明了我们项目里光照系统的分工。
flowchart TD
A["大世界场景物体"] --> B{是否移动或可破坏}
B -->|"是"| C["动态光照路径"]
B -->|"否"| D["静态光照路径"]
C --> C1["Lumen 动态 GI"]
C --> C2["CSM / VSM 动态阴影"]
C --> C3["Light Function / IES 点缀"]
D --> D1["GPU Lightmass 烘焙 lightmap"]
D --> D2["Distance Field 远影"]
D --> D3["预计算天空光反射"]
C2 --> E["最终合成画面"]
C1 --> E
D1 --> E
D2 --> E
D3 --> E选择哪种方案,要看项目的动态程度。如果是一个昼夜循环、天气变化的开放世界,动态部分占比会很高,Lumen 和 CSM/VSM 的投入就值得。如果是一个静态风景展示关卡,大量烘焙可能更划算。
五、烘焙与动态光照的权衡:没有免费午餐
大世界光照里最难的不是技术选择,而是算账。每一盏灯、每一个物体,都要决定它是静态还是动态,是用烘焙阴影还是实时阴影。
UE 里灯光的 Mobility 有三种:Static、Stationary、Movable。Static 灯完全烘焙,运行时无开销。Stationary 灯可以烘焙间接光和静态阴影,同时保留动态阴影和颜色变化能力。Movable 灯完全实时,最灵活也最贵。
一个常见的错误是太阳也设成 Movable。太阳作为主方向光,如果完全动态,那整个场景的 CSM/VSM 都要实时计算。大世界场景物体多,这个开销非常大。更好的做法是把太阳设成 Stationary,烘焙静态间接光和远距离静态阴影,只让中近距离的动态物体投射动态阴影。
但 Stationary 灯有一个限制:同一片区域里不能有太多 Stationary Light 重叠。UE 默认一个 primitive 上最多叠加 4 盏 Stationary Light 的动态阴影,超出的会被降级或忽略。大世界里的路灯、火把、营火如果都设成 Stationary,很容易超限制。我们的做法是:
- 太阳:Stationary,烘焙远距离光照,保留动态阴影给中近距离。
- 路灯和火把:Static,纯烘焙,除非玩家能点燃或熄灭。
- 玩家技能、爆炸、可携带光源:Movable,完全动态。
这种分级让大部分光源走烘焙,只有真正需要交互的光源才走动态。阿杰的山谷里有三百多盏小光源,如果全动态,GPU 时间会增加 3ms 以上。按上述分级后,只有不到 10% 的光源是动态的。
另一个权衡是阴影精度与距离。动态阴影贴图的分辨率是固定的,远处物体在贴图上只占很少像素。如果想让远处阴影看起来不碎,要么提高贴图分辨率,要么减少阴影覆盖距离。前者增加内存和填充开销,后者导致远处没有阴影。通常的妥协是:
- CSM 负责近处高精度动态阴影。
- Distance Field Shadow 或烘焙阴影负责中远距离。
- 更远处用材质 fake shadow 或 lightmap 里的烘焙阴影。
这个权衡可以用一个简单的不等式描述。设阴影贴图分辨率为 ,阴影覆盖距离为 ,则世界空间每 texel 覆盖的尺寸为:
为了让阴影边缘不出现明显锯齿,通常要求 不超过目标物体在远处投影的最小可接受宽度 。例如一块远处岩石在地面上的投影宽度为 50 cm,如果 已经达到 30 cm,阴影边缘就会和物体本身一样宽,完全失去意义。这时要么增大 ,要么缩短 。
六、性能预算:光照开销要有上限
大世界项目必须在立项时就给光照定一个性能预算。我们建议按平台拆分:
- PC 高端:GPU 时间 4.0ms 以内,含 shadow、GI、reflection。
- PC 主流:GPU 时间 6.0ms 以内。
- 主机:按具体 SOC 定,通常 5.0ms 到 7.0ms。
这个预算要进一步拆成 shadow、GI、lighting、post-process 几部分。 shadow 通常占光照预算的 40% 到 60%。阿杰的 PC 高端目标是 shadow 不超过 2.5ms,GI 不超过 1.5ms。
为了守住预算,我们做了几件事:
- 限制动态光源数量。同屏 Movable Light 不超过 8 盏,关键路径上不超过 4 盏。
- 限制动态阴影距离。Directional Light 的 Dynamic Shadow Distance 按场景分段,山谷 8000,森林 6000,平原 10000。
- 开启阴影 LOD。远距离物体用低精度阴影或不投射阴影。
- 对草地、树叶等高频物体关闭 Cast Shadow 或改用 contact shadow。
- VSM 只在 Directional Light 和关键 Spot Light 上开,点光源用传统 shadow map。
还有一个容易被忽略的开销是阴影过滤。UE5.8 默认对 CSM 使用 PCSS 或 PCF 软阴影,采样数越高越柔和也越贵。大世界远处阴影本来就不需要很锐利,可以把 Directional Light 的 Shadow Filter Shrink 或 Filter 半径调小,减少采样数。
我们用控制台命令 stat gpu 和 stat shadowrendering 来监控 shadow 开销。r.Shadow.DistanceFieldStochasticHaltonSamples 可以控制 DF shadow 的采样数,默认值通常够用,性能吃紧时可以降到 8 或 4。
下面这张图是我们给光照定的预算框架。
graph LR
A["每帧 GPU 预算"] --> B["Shadow 40-60%"]
A --> C["GI 15-30%"]
A --> D["直接光照 10-20%"]
A --> E["Reflection 5-10%"]
B --> B1["CSM/VSM 动态阴影"]
B --> B2["Distance Field 远影"]
B --> B3["烘焙静态阴影"]
C --> C1["Lumen Trace"]
C --> C2["Lightmap 采样"]
D --> D1["Shading Model"]
D --> D2["Light Attenuation"]
E --> E1["SSR / Reflection Capture"]
E --> E2["Lumen Reflection"]预算定了之后,美术摆灯就要按规矩来。我们会在关卡里放几个 benchmark 相机位,每天早上跑一遍 GPU profiler,看有没有超预算。超了就回去砍光源或调距离。
七、阴影距离与精度:从数学到手感
阴影距离和精度的关系前面已经提过,但实际操作中还有几个细节要考虑。
第一个是 Cascade Transition Fraction。它控制相邻两级 CSM 之间的重叠比例。默认值 0.1 表示两级之间有 10% 的重叠区域用来做渐变过渡。如果设得太小,过渡会很硬,玩家移动时能看到阴影突然切换。如果设得太大,两级重叠区域变宽,有效分辨率被浪费。大世界建议 0.1 到 0.2 之间。
第二个是 Cascade Distribution Exponent。它的作用是按指数分布把视锥空间分给各级联。Exponent 越大,近处级联占的视锥比例越大。设成 2.0 时,第一级可能覆盖 0 到 1000 cm,第二级 1000 到 3000 cm,第三级 3000 到 8000 cm。设成 3.0 时,第一级可能覆盖 0 到 500 cm,第二级 500 到 1500 cm,第三级 1500 到 4000 cm。
这个参数没有标准答案,要看场景内容分布。如果场景近处有很多高频细节,比如碎石、草丛、栏杆,就把 Exponent 调大,让近处级联覆盖更多距离。如果中距离有大量建筑,就把 Exponent 调小,让中级联更宽。
第三个是 Shadow Bias。Bias 太小会出现 shadow acne,表面上出现条纹状阴影。Bias 太大会出现 peter-paning,阴影和物体根部脱节。大世界地形起伏大,Bias 要针对地形材质和太阳角度反复调。阿杰的山谷里,太阳低角度时 Bias 要增大,否则悬崖表面全是 acne。
第四个是 Contact Shadows。这是一种屏幕空间接触阴影,适合表现物体与地面的细小接触阴影,比如角色脚下、桌椅腿。它的开销比 CSM 小很多,但只在屏幕空间有效,远处或屏幕边缘会消失。大世界建议给角色和重要近处物体开启,细节提升明显。
UE5.8 里 Contact Shadows 的开关在 Directional Light 和每个 Light Component 上都能找到。距离建议 150 到 300 厘米,太远了会穿帮。
下面这张图说明了我们项目里不同距离段使用的阴影技术。
sequenceDiagram
participant Cam as "摄像机"
participant CS as "Contact Shadows"
participant CSM as "CSM 级联阴影"
participant DFS as "Distance Field Shadow"
participant Bake as "烘焙 Lightmap"
participant Horizon as "远景 Fake Shadow"
Cam->>CS: 0 - 200 cm,细小接触阴影
Cam->>CSM: 0 - 8000 cm,主要动态阴影
Cam->>DFS: 8000 - 30000 cm,远处静态物体阴影
Cam->>Bake: 全场景,静态基底阴影
Cam->>Horizon: > 30000 cm,地形剪影与氛围八、UE5.8 光照改进:Epic 在大世界上做了哪些事
UE5.8 相比 5.4 在光照和阴影上有几个值得关注的改进。
第一是 VSM 的稳定性。5.0 到 5.2 时期 VSM 的漏阴影、闪烁和崩溃问题比较多。5.8 里 Epic 改进了 page cache 的管理,Nanite 与 VSM 的协同也更稳定。我们在山谷里跑了几个小时的 camera fly-through,VSM 没有出现过之前那种整帧黑块。
第二是 Lumen 的远距离性能。5.8 对 Lumen 的 tracing 做了优化,远处 SDF 的采样更聪明,irradiance cache 更新更省。我们在平原场景里测过,Lumen 的 GPU 时间比 5.4 低了约 15%。
第三是 GPU Lightmass 的速度。5.8 的 GPU Lightmass 支持更好的分布式烘焙和渐进式预览,大世界关卡的反复调试时间缩短了。阿杰之前烘焙一次山谷要 45 分钟,5.8 里降到 20 分钟左右。
第四是 MegaLights。这是 UE5.8 引入的新光源系统,目标是让数百盏动态光源在复杂场景里也能高效工作。它用一种新的 cluster 和 deferred 光照路径,大量减少传统 tiled deferred 的 overdraw。大世界里的夜景、城市场景可能会很受益。不过我们项目还没完全切换到 MegaLights,只在测试关卡里跑过,效果值得期待。
第五是阴影的可扩展性设置。5.8 的控制台变量更丰富,比如 r.Shadow.Virtual.UsePersistentPageCache 可以让 VSM 在相机移动时复用上一帧的 page,减少抖动;r.Shadow.Virtual.MaxPhysicalPages 可以更细粒度地控制内存占用。
这些改进不会自动解决大世界的所有光照问题,但它们让分层策略更容易实现。阿杰现在做新关卡时,会先画一张阴影分层图,再决定每段用什么技术,而不是一上来就拉参数。
结语: shadow 是一门距离的艺术
阿杰的山谷最后稳定在 PC 高端 58 帧、主流 45 帧。阴影方案就是前面说的分层:CSM 近中、DF 远、烘焙基底、VSM 给关键灯光。不是每一帧都把阴影画到地平线,而是让玩家觉得阴影画到了地平线。
大世界光照和阴影的核心,其实是控制观众的目光。玩家不会真的盯着两公里外一块石头的阴影看,他们看的是整体轮廓、光影节奏、远近层次。技术方案要服务于这个感受,而不是追求每一处都物理正确。
UE5.8 给了我们很多工具,但工具越多,越需要清醒的分配策略。把阴影画到该画的地方,用合适的精度,守住性能预算,才是大世界光照的正经做法。