虚拟阴影贴图

自动关联目录:虚拟阴影贴图

官方依据: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 继续提升分辨率

四个设计目标

  1. 大幅提升阴影分辨率,匹配高细节 Nanite 几何
  2. 合理的软阴影,且性能代价可控
  3. 开箱即用,几乎不需要调
  4. 用统一的路径取代原来一堆 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.RayCountLocal8(Shadows= Epic)局部光的光线数。少 → 半影可见噪点;设 0 → 关闭 SMRT,退回单采样硬阴影
r.Shadow.Virtual.SMRT.RayCountDirectional8方向光同上
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.VSMInvalidateOnLODDeltaNanite 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.AsyncComputeprofile 时设 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.Layout0 全屏 / 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见下

怎么查

  1. Cached Page 可视化:绿的=完全缓存,红的=新增或作废,蓝的=只缓存了静态页。移动相机时屏幕边缘、clipmap 边界、被遮挡后又露出的地方出现少量红环是正常的;静止相机时红色应基本来自作废。
  2. ShadowCasters 视图:看是哪些对象及其包围盒在驱动作废,确认包围盒是否足够紧。

因为"包围盒可能重叠到的所有页都要作废",中等程度的包围盒膨胀 + 低角度光照,会造成大量不必要的作废。

关键优化点

  • Nanite 遮挡住的作废可以跳过,非 Nanite 不行 → 大型投影体(建筑之类)务必用 Nanite
  • 骨骼动画 / WPO / PDO 的几何每帧作废,必须严控包围盒
  • WPO 的位移在渲染前是未知的,缓存系统只能假定它需要重画——即位移其实没变也一样。能用骨骼动画或直接改实例 transform 就别用 WPO
  • 静态/动态分离缓存:VSM 存两份深度。动态几何移动时不必重画静态几何,只需把静态页便宜地合成上去。
    • 判定在运行时做,不主要看 mobility:静态网格 + Static → 静态缓存;Movable 但长期不更新 → 会被挪进静态缓存
    • 健康指标:静态页的 Invalidated 数应接近 0;频繁作废静态页的实例应改成 Movable

6. 非 Nanite 几何的优化

官方给出的清单:

  1. 尽可能开 Nanite(包括低模)——Nanite 渲染进 VSM 效率高得多,且能遮挡非 Nanite 几何、避免虚假作废。理想状态下只有 Nanite 不支持的才用非 Nanite
  2. 非 Nanite 必须有完整的 Mesh LOD 层级,否则渲染进小页会极贵
  3. 用 r.Shadow.RadiusThreshold 控制远处小物体
  4. 方向光在 Dynamic Shadow Distance Movable Light 之外切到 Distance Field Shadows(非 Nanite 切换,Nanite 仍保持全细节)
  5. 关闭非 Nanite 在 Coarse Pages 里的渲染,常能拿到很大收益
  6. 试 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

参考