虚拟阴影贴图
自动关联目录:虚拟阴影贴图
官方依据:Virtual Shadow Maps(UE 5.8 Documentation)。本页覆盖该官方页全部 37 个条目,并补官方手册不写的成因解释、CVar 汇总与排查顺序。
1. 定位
虚拟阴影贴图(VSM)是为影视级资产与大型动态光照开放世界设计的高分辨率阴影方案,与 Nanite、Lumen、World Partition 配套。
一句话定位:VSM 就是一张极其巨大的阴影贴图(虚拟分辨率 16k×16k),切成 128×128 的页,只渲染屏幕上真正需要的那些页,并在帧间缓存。整个系统的性能问题,本质上都是**"页"的问题**——要么页太多,要么页被反复作废。
官方明确的前提:VSM 是为 Nanite 设计的,目标平台或项目不用 Nanite 的话,VSM 很可能不合适。
2. 结构
核心机制
虚拟分辨率 16k × 16k
├─ 切成 128×128 的 Page(页)
├─ 按深度缓冲分析,只为"屏幕上要着色的像素"分配并渲染页
├─ 页在帧间缓存,除非被移动物体或光源作废(invalidate)
└─ 方向光额外用 Clipmap 继续提升分辨率四个设计目标
- 大幅提升阴影分辨率,匹配高细节 Nanite 几何
- 合理的软阴影,且性能代价可控
- 开箱即用,几乎不需要调
- 用统一的路径取代原来一堆 Stationary Light 阴影技术
启用
| 项 | 说明 |
|---|---|
| 位置 | Project Settings → Rendering → Shadows → Shadow Map Method |
| 新项目 | 默认启用 VSM |
| UE5.0 之前创建的项目 | 需手动 opt-in,或用 CVar r.Shadow.Virtual.Enable |
启用后会取代哪些旧方案
| 被取代 | |
|---|---|
| Stationary 预计算阴影(含 2D Distance Field、shadow factor "shadow maps") | ✅ |
| Pre-shadows | ✅ |
| Per-object / inset shadows | ✅ |
| 级联阴影贴图(CSM) | ✅ |
| 局部光的可移动动态阴影 | ✅ |
| 不被取代 | 说明 |
|---|---|
| 完全烘焙的 Static Light 阴影 | 仍然照常工作(不用 Lumen 时),因为贡献全在烘焙 lightmap 里,运行时根本不评估光照 |
| Distance Field Shadows | 未被取代,可与 VSM 并行用于方向光:在 Dynamic Shadow Distance Movable Light 之外接管非 Nanite 几何 |
| Stationary 光的间接漫反射 | 仍来自烘焙 lightmap;但直接光与阴影在启用 VSM 后按动态计算(等同 Movable) |
其它要点:
- Nanite 几何无论多远都渲染进 VSM(官方认为这是性能最好、质量最高的选择);让非 Nanite 也一样可用
r.Shadow.Virtual.UseFarShadowCulling 0 - 局部光(Point/Spot)不受影响,为它们选 DF Shadows 仍会覆盖 VSM
- Contact Shadow Length 不再必要——VSM 分辨率足够高,接触阴影已经很锐利;只在"对象未渲染进阴影贴图、想捡便宜阴影"时还有点用,否则不推荐(不如 VSM 准)
- 光追阴影优先级高于 VSM(质量最高)
3. 软阴影:SMRT
Shadow Map Ray Tracing(SMRT) 是配合 VSM 产生可信软阴影与接触硬化的采样算法:投影越远的物体阴影越软,越接近接收面越锐利。
原理:朝光源发射若干条光线,但不去和几何求交——而是沿光线取若干采样点,投影到虚拟阴影贴图上去测试,从而得到软阴影与接触硬化。光线分布由光源的 Source Radius(局部光)或 Source Angle(方向光)决定。
局部光默认没有 Source Radius;方向光起始有一个较低的 Source Angle。给它们设了合适的值,SMRT 才产出实时软阴影。
过去的 PCF 方案会过度模糊,削弱高分辨率几何与阴影的视觉冲击。
4. 参数与控制台变量(本页最实用的一段)
软阴影质量
| CVar | 默认 | 作用 |
|---|---|---|
r.Shadow.Virtual.SMRT.RayCountLocal | 8(Shadows= Epic) | 局部光的光线数。少 → 半影可见噪点;设 0 → 关闭 SMRT,退回单采样硬阴影 |
r.Shadow.Virtual.SMRT.RayCountDirectional | 8 | 方向光同上 |
r.Shadow.Virtual.SMRT.SamplesPerRayLocal | — | 每条光线的采样数,控制最大软度;4–8 效果不错 |
r.Shadow.Virtual.SMRT.SamplesPerRayDirectional | — | 同上 |
r.Shadow.Virtual.SMRT.MaxRayAngleFromLight | — | 放宽/收紧半影的钳制范围 |
r.Shadow.Virtual.SMRT.RayLengthScaleDirectional | — | 同上(方向光) |
Clipmap(方向光)
| CVar | 说明 |
|---|---|
r.Shadow.Virtual.Clipmap.FirstLevel | 首层,默认 6(最近处约 64 cm) |
r.Shadow.Virtual.Clipmap.LastLevel | 末层,默认 22(约 40 km) |
r.Shadow.Virtual.ResolutionLodBiasDirectional | -1 等于分辨率翻倍;0 表示按透视投影自动选 |
每层都是一张 16K VSM,半径逐层翻倍。空层的成本可以忽略,所以默认配置能覆盖非常大的场景。
局部光
- Spot Light:单张 16k VSM + mip 链(不用 clipmap)
- Point Light:6 面 cubemap,每面一张 16k VSM
r.Shadow.Virtual.ResolutionLodBiasLocal调整分辨率偏置- 目前没有逐光源的分辨率控制(官方说未来可能加)
缓存与作废
| CVar / 属性 | 作用 |
|---|---|
r.Shadow.Virtual.Cache | 设为 0 可关闭缓存(调试用) |
Shadow Cache Invalidation Behavior(逐 primitive) | Auto(默认)/ Always / Rigid(抑制 WPO 引起的作废)/ Static(再叠加抑制 transform 变化;移动它结果未定义) |
r.Nanite.VSMInvalidateOnLODDelta | Nanite LOD 流送变化时触发 VSM 作废。实验性,不建议随项目发布 |
World Position Offset Disable Distance(primitive 属性) | 超过该距离不再应用 WPO |
| Displacement Fading(材质) | 同上,材质侧淡出 |
移动光源
| CVar | 作用 |
|---|---|
r.Shadow.Virtual.ResolutionLodBiasLocalMoving | 移动中的局部光用不同 LOD bias |
r.Shadow.Virtual.ResolutionLodBiasDirectionalMoving | 移动中的方向光;光源停下后会渐变回原 bias |
Coarse Pages
粗页用于体积雾、前向渲染半透明这类"需要在任意位置采样阴影"的系统。
| CVar | 作用 |
|---|---|
r.Shadow.Virtual.MarkCoarsePagesLocal | 局部光粗页开关 |
r.Shadow.Virtual.MarkCoarsePagesDirectional | 方向光粗页开关 |
r.Shadow.Virtual.FirstCoarseLevel / LastCoarseLevel | 粗页标记的 clipmap 层范围 |
粗页可能带来性能问题,尤其是非 Nanite 动态几何——等于在大片空间上渲染低分辨率阴影,容易变成 draw call 瓶颈。
其它
| CVar | 作用 |
|---|---|
r.Shadow.Virtual.Enable | 启用 VSM |
r.Shadow.Virtual.UseFarShadowCulling | 设为 0 让非 Nanite 也无视距离渲染进 VSM |
r.Shadow.Virtual.MaxPhysicalPages | 物理页池大小(见"溢出"一节) |
r.Shadow.Virtual.NormalBias | 默认 0.5,缓解低模的 shadow terminator 问题 |
r.Shadow.Virtual.MaxDOFResolutionBias | 景深虚化区域可降低阴影分辨率(过场镜头收益大) |
r.Shadow.Virtual.TranslucentQuality | >1 开启半透明高质量阴影过滤(性能影响大),0 为默认 |
r.Shadow.Virtual.ForceOnlyVirtualShadowMaps | 设 0 会为不支持的几何重新启用旧阴影路径(不推荐,已弃用) |
r.Shadow.Virtual.MarkPixelPagesMipModeLocal | 设 1 或 2 减少局部光分配的 mip 数,改善非 Nanite 性能 |
r.Shadow.RadiusThreshold | 非 Nanite 的 CPU 剔除阈值 |
r.RDG.AsyncCompute | profile 时设 0 能得到可信的 GPU 计时(仅用于分析,发布版绝不能关) |
r.ShaderPrintEnable + r.Shadow.Virtual.Stats | 依次开启后显示页统计 |
r.ShaderPrintEnable + r.Shadow.Virtual.ShowStats 2 | 显示物理页池占用 |
可视化
View Modes → Virtual Shadow Map,可选:Shadow Mask、Clipmap/Mip Level、Virtual Page、Cached Page、Nanite Overdraw、Shadow Casters。
| CVar | 说明 |
|---|---|
r.Shadow.Virtual.Visualize [mode] | mask / mip / vpage / cache / naniteoverdraw / raycount / clipmapvirtual;none 关闭 |
ShowFlag.VisualizeVirtualShadowMap | 开启 VSM 可视化 |
ShowFlag.VisualizeShadowCasters | 开启 ShadowCasters 视图 |
r.Shadow.Virtual.Visualize.Layout | 0 全屏 / 1 缩略图 / 2 分屏 |
r.Shadow.Virtual.Visualize.DumpLightNames | 输出当前带 VSM 的光源列表 |
r.Shadow.Virtual.Visualize.LightName / NextLight / PrevLight | 指定/切换要查看的光源 |
r.Shadow.Virtual.Visualize.ShadowCachedPagesOnly | 只显示已缓存页 |
开启可视化会轻微但可测量地影响 VSM 性能,profile 前务必关掉。
5. 缓存作废:VSM 性能的主战场
什么会作废页
| 来源 | 影响 |
|---|---|
| 光源移动或旋转 | 该光源的全部缓存页作废 |
| 投影几何移动 / 增删 | 从光源视角看,与其包围盒重叠的页作废 |
| 蓝图设置 primitive 组件属性触发 render state 失效(即使物体没动) | 同上 |
| 材质用了 WPO / PDO | 见下 |
怎么查
- Cached Page 可视化:绿的=完全缓存,红的=新增或作废,蓝的=只缓存了静态页。移动相机时屏幕边缘、clipmap 边界、被遮挡后又露出的地方出现少量红环是正常的;静止相机时红色应基本来自作废。
- ShadowCasters 视图:看是哪些对象及其包围盒在驱动作废,确认包围盒是否足够紧。
因为"包围盒可能重叠到的所有页都要作废",中等程度的包围盒膨胀 + 低角度光照,会造成大量不必要的作废。
关键优化点
- Nanite 遮挡住的作废可以跳过,非 Nanite 不行 → 大型投影体(建筑之类)务必用 Nanite
- 骨骼动画 / WPO / PDO 的几何每帧作废,必须严控包围盒
- WPO 的位移在渲染前是未知的,缓存系统只能假定它需要重画——即位移其实没变也一样。能用骨骼动画或直接改实例 transform 就别用 WPO
- 静态/动态分离缓存:VSM 存两份深度。动态几何移动时不必重画静态几何,只需把静态页便宜地合成上去。
- 判定在运行时做,不主要看 mobility:静态网格 + Static → 静态缓存;Movable 但长期不更新 → 会被挪进静态缓存
- 健康指标:静态页的 Invalidated 数应接近 0;频繁作废静态页的实例应改成 Movable
6. 非 Nanite 几何的优化
官方给出的清单:
- 尽可能开 Nanite(包括低模)——Nanite 渲染进 VSM 效率高得多,且能遮挡非 Nanite 几何、避免虚假作废。理想状态下只有 Nanite 不支持的才用非 Nanite
- 非 Nanite 必须有完整的 Mesh LOD 层级,否则渲染进小页会极贵
- 用
r.Shadow.RadiusThreshold控制远处小物体 - 方向光在
Dynamic Shadow Distance Movable Light之外切到 Distance Field Shadows(非 Nanite 切换,Nanite 仍保持全细节) - 关闭非 Nanite 在 Coarse Pages 里的渲染,常能拿到很大收益
- 试
r.Shadow.Virtual.MarkPixelPagesMipModeLocal设 1 或 2
实例数放大效应(很反直觉):单个 Static Mesh 实例,对每个与之相交的点光源会产生 48 个实例(8 mip × 6 cubemap 面);对方向光产生 17 个(默认 17 个 clipmap 层)。
7. 性能剖析
VSM 的成本分两大块,彼此相对独立:
| 类别 | 是什么 | 取决于 |
|---|---|---|
| Shadow Depths | 把几何渲染进阴影贴图的成本 | 要渲染多少页、页里要画多少几何 |
| Shadow Projection | 用 SMRT 采样阴影贴图的成本 | 全屏总共采了多少阴影样本(与页数、缓存无关) |
- Shadow Depths 里有
RenderVirtualShadowMaps(Nanite)(所有方向光一个 pass,所有局部光第二个 pass)与RenderVirtualShadowMaps(Non-Nanite)(每个可见光单独 pass) - Shadow Projection 在
Lights | DirectLighting | UnbatchedLights,最贵的是VirtualShadowMapProjection里的 SMRT 主循环 - 工具:GPU Profiler、Unreal Insights(
trace.enable counters,vsm)、CSV(CsvCategory VSM enable) - 注意:
stat gpu之类的计时在 CPU-bound 时不可靠
页统计怎么看
依次执行 r.ShaderPrintEnable 1 → r.Shadow.Virtual.Stats 1(或 basic 只看页统计):
| 项 | 含义 |
|---|---|
| Physical Pages | 可用物理页上限 |
| Allocated | 当前视图请求的页数,必须小于 Max pages,否则会损坏 |
| Cleared | 本帧新清空的页 |
| Cached | 已在缓存中、本帧无需渲染的页(成本极低);启用静态分离缓存后拆成 Static / Dynamic |
| Invalidated | 被动态几何作废、需要重画的页。静态页的这一项理想应为 0 |
| Merged | 静态页与动态页合并的页数 |
8. 已知限制(逐条,这是能否使用的判断依据)
| 限制 | 说明 |
|---|---|
| 对象包围盒 | 太小 → 阴影缺失或错误;太大 → 过度作废。用 WPO 的材质要在材质里设 Max World Position Offset Displacement 或扩展网格包围盒 |
| 多光源性能 | 大量小局部光的场景仍在改进中;当前策略是开 one pass projection、严控作废。多局部光下 Nanite 远优于非 Nanite |
| 低多边形几何 | "shadow terminator problem"(几何法线与着色法线不一致导致终界线附近出现瑕疵)。最好加面(用 Nanite 加面很便宜);实在不行调 r.Shadow.Virtual.NormalBias(默认 0.5),但这会牺牲全场景阴影质量,只作最后手段 |
| 物理页池溢出 | 所有阴影数据存在一个大纹理池里。页数超过 Max Pages 会损坏,表现为棋盘格图案、阴影错乱或缺失,并在屏幕与日志报警。用 r.Shadow.Virtual.MaxPhysicalPages 调大,或降低阴影分辨率/减少投影光源 |
| 材质 | 只支持简单次表面;Subsurface Profile 与 transmission 尚未实现,用了会按不透明处理 |
| 阴影分辨率 | 浅角度光照(投影走样)与超大局部光会耗尽虚拟分辨率,表现为方块状阴影与 bias 问题。没有简单解法,只能靠设计规避 |
| 不支持的几何 | 少数几何类型默认不投影;r.Shadow.Virtual.ForceOnlyVirtualShadowMaps 0 可重新启用旧路径(不推荐,已弃用) |
| Map Check 警告 | 启用 VSM 后"Lighting needs to be rebuilt"提示不会出现(但 Stationary 的间接光仍是烘焙的);关于 preshadow 的警告可忽略 |
半影的两个固有限制
| 限制 | 成因 |
|---|---|
| 半影尺寸被钳制 | 避免采样相对光线原点发散过度("弯曲")。值过大会影响性能,且相机靠近时半影会视觉扭曲 |
| 半影不一致 / 漏光 | VSM 只存第一层深度,朴素迭代会漏掉第一个遮挡体之后的遮挡体。官方用"补洞启发式"——根据遮挡点之前看到的深度去外推后面的深度来缓解 |
平台
PS5、Xbox Series S|X,以及 PC(最新驱动 + DirectX 12):
- NVIDIA Maxwell 及以后;AMD GCN 及以后
- Windows 10 需新于 1909.1350(2004/20H2 需 ≥ .789),Windows 11 需支持 DX12 Agility SDK
- DX12(Shader Model 6.6 atomics)或 Vulkan(VK_KHR_shader_atomic_int64)
- Apple Silicon M2+;Linux 需 NVIDIA 2080+
9. 代价与权衡
| 权衡 | 说明 | 什么时候不该用 |
|---|---|---|
| 为 Nanite 而生 | 不用 Nanite 时非 Nanite 渲染进 VSM 极贵 | 项目不用 Nanite → 官方直接说 VSM 很可能不合适 |
| 取代一整批旧方案 | 统一路径,但也意味着旧调参经验作废 | 已有重度 CSM 调优的项目要重新调 |
| 缓存是性能核心 | 一切优化围绕"减少作废" | 大量动态/形变几何的场景成本高 |
| 固定显存占用 | 单一大纹理池,可能溢出 | 低端硬件需调小页池 |
| 多小光源仍弱 | 官方承认在改进中 | 大量小局部光的场景要实测 |
| 光追阴影优先 | 光追开启时 VSM 被让位 | 已用光追阴影就不必纠结 VSM 质量 |
10. 排查顺序
1. 是否用了 Nanite?没用 → 先评估 VSM 是否合适
2. 页池是否溢出?(r.ShaderPrintEnable 1 + r.Shadow.Virtual.ShowStats 2)
└─ 溢出 → 调大 MaxPhysicalPages / 降分辨率 / 减少投影光源
3. Cached Page 可视化:红色多不多?
└─ 多 → ShadowCasters 视图找是谁在作废 → 收紧包围盒 / 改 Mobility / 用 Shadow Cache Invalidation Behavior
4. 静态页 Invalidated 是否接近 0?
└─ 不是 → 把频繁作废的实例改成 Movable
5. WPO/PDO/骨骼动画的包围盒是否失控?(有 WPO 要设 Max WPO Displacement)
6. profile(记得 r.RDG.AsyncCompute 0 且关掉可视化)
├─ Shadow Depths 高 → 页数/几何量,走 Nanite 与剔除
└─ Shadow Projection 高 → 降低 Source Radius/Angle、ray count、samples per ray