Avowed: A GPU Technical Retrospective | Unreal Fest Orlando 2025
Avowed: A GPU Technical Retrospective | Unreal Fest Orlando 2025
项目与团队概况
游戏简介
- Avowed 是一款 单人动作 RPG ,设定在 《Pillars of Eternity(永恒之柱)》 宇宙中
- 支持 第一人称 / 第三人称 切换游玩
- 战斗系统融合 近战、远程、法术 三种模式,重点围绕 剧情探索 与 跑酷/导航
- 发售平台: Xbox Series S/X 以及 PC
- 地图规模:约 5 个大型开放区域 + 约 25 张硬加载(hard-loaded)地图
团队规模
- 峰值约 100 名内部开发者 + 40 名外部协作开发者
- 技术团队约 15 人 ,细分为:
- 1 名技术总监
- 2 名内部图形工程师 + 2 名外部图形协作开发者
- 5 名内部引擎 & 工具程序员 + 2 名外部协作
- 3 名技术美术 + 2 名灯光师
- 工程人力相对紧张,因此在 引擎修改方向的选择上必须非常精准
项目技术演进与目标
引擎版本迁移
- 项目最早在 UE4(4.22 / 4.23) 上启动
- 最终以 Unreal Engine 5.3.2 发售
核心项目目标
- 开放或近开放世界 :支撑大型远景(vast vistas)
- 全动态时间变化(Time of Day) :不依赖大量预烘焙光照,需要 全局光照(GI) 支持
- 高度有机化的世界 :大量植被、洞穴等自然场景
采用的 UE5 核心技术
| 技术 | 用途 |
|---|---|
| Nanite | 几何体 LOD & 流送 |
| Lumen | 全局光照 & 反射 |
| Virtual Shadow Maps (VSM) | 阴影系统 |
| Virtual Textures | 纹理流送 |
| Niagara | 粒子系统 |
本次演讲重点聚焦前三项(Nanite、Lumen、VSM),附带简要提及 Virtual Textures。
各平台渲染配置详解
Xbox Series S
| 模式 | 帧率 | 内部分辨率 | Lumen 模式 | 阴影方案 | 反射方案 |
|---|---|---|---|---|---|
| Quality | 30 Hz | 900p | 硬件光追 Lumen | VSM(方向光)+ 硬件光追 (局部光) | 硬件光追反射 (Lumen + 单层水面) |
| Balance | 40 Hz | 720p | 硬件光追 Lumen | VSM(方向光)+ 硬件光追(局部光) | 光追反射(Lumen)+ 屏幕空间反射 SSR (单层水面) |
Xbox Series X
| 模式 | 帧率 | 内部分辨率 | Lumen 模式 | 阴影方案 | 反射方案 |
|---|---|---|---|---|---|
| Quality | 30 Hz | 1440p | 硬件光追 Lumen | VSM(方向光)+ 硬件光追(局部光) | 全硬件光追反射 |
| Balance | 40 Hz | 1280p | 硬件光追 Lumen | VSM(方向光)+ 硬件光追(局部光) | 同上规格 |
| Performance | 60 Hz | 1080p ~ 1800p | 软件 Lumen | 全 VSM (方向光 + 局部光) | 软件 Lumen 反射 (含单层水面) |
关键点 :Performance 模式退回软件 Lumen 并非因为 GPU 瓶颈 ,而是 CPU 限制 所致,这决定了他们在该模式下放弃硬件光追。
Nanite 深入分析
Nanite 的核心价值
- Avowed 几乎对所有资产都启用了 Nanite ,这是项目成功的 最大技术贡献者之一
- 三大优势:
- 远景支持 :巨大视野下几乎 无 LOD 弹跳(LOD popping)
- 内存控制 :主机端内存始终是严峻挑战,Nanite 的 mesh 流送(streaming)与裁剪(culling)能力对 内存预算控制至关重要
- 自动化 LOD 管理 :减少手工 LOD 链的维护成本
从 UE4 迁移到 Nanite 的挑战
1. 大量 Masked Material 的转换
- UE4 遗留资产中广泛使用了 Masked 材质 (带 alpha test 的裁切材质),几乎所有资产都有"frilly bits"(透明边缘碎片)
- 需要将这些 转换为不透明(Opaque)几何体 才能充分发挥 Nanite 性能
- 这一工作由 美术团队和技术美术团队 联合完成,耗费 大量工时
2. Spline Mesh 的处理
- 项目大量使用了 Spline Mesh (样条网格,例如道路、藤蔓等)
- 当时 Nanite 不支持 Spline Mesh ,因此必须将其 烘焙为静态几何体
Nanite 的性能瓶颈分析
材质复杂度过高
- 转换为 Opaque 后,Nanite 的主要瓶颈转移到了 材质(Material) 层面
- UE4 时代因为几何量较少,习惯在材质中做大量运算(多纹理采样、复杂节点图),迁移后这些 过度复杂的材质 成为负担
- Nanite 本身的 VGPR(向量通用寄存器) 和 LDS/BLU(带宽/局部存储) 占用已经较高
- 叠加复杂材质后,Occupancy(占用率,即 GPU 上同时运行的 wavefront 比例)始终偏低 ,无法充分隐藏延迟
Occupancy 是 GPU 性能的关键指标。VGPR 用量越高 → 每个 wavefront 占据的寄存器越多 → 可并发的 wavefront 数量越少 → occupancy 越低 → 延迟隐藏能力越差 → 性能下降。
Virtual Texture 导致纹理地址绑定(Texture Address Bound)
- 由于大量使用 Virtual Textures(VT) ,Nanite 的 base pass 频繁出现 纹理地址计算瓶颈
- VT 的间接寻址(indirection)比普通纹理采样更昂贵,加剧了这一问题
缺乏 UE 5.4+ 的优化特性
- 项目基于 UE 5.3 ,无法使用后续版本引入的:
- Nanite Compute Shading :用计算着色器替代像素着色器来做材质评估,减少冗余计算
- VRS(Variable Rate Shading,可变速率着色) :降低非关键区域的着色频率
- 因此所有像素 逐像素(pixel per pixel)全精度着色 ,没有任何节省手段
空 Draw Call 问题
- 材质变体主要通过 Material Instances 实现,而非 Custom Primitive Data(CPD)
- 后果:base pass 中产生大量 被裁剪掉但仍要发出的空 draw call(empty base pass draws)
- 原因:在 UE 5.3 中,只要 mesh 被加载到内存,Nanite 就必须为其发出 draw call ,即使它完全被裁剪(culled away)
- 实际表现:经常有 100+ 纳秒甚至更多 的 GPU 时间浪费在空 draw call 上
- 未来的 Work Graphs 技术有望解决此问题(通过 GPU 驱动的工作调度避免空 dispatch)
Nanite Culling 过载与流送策略调整
Culling Pass 过载
- 即使有裁剪,很多时候 流入场景的实例数量仍然过多 ,导致 Nanite 的 culling passes 本身开销过大
HLOD 烘焙不足
- 早期项目没有严格执行 HLOD(Hierarchical LOD,层级 LOD) 烘焙
- 后期 强制推行 HLOD 后,性能有明显改善
- HLOD 的作用:将远处的大量小物体合并为少量简化 mesh,减少 Nanite 需要处理的 instance 数量
Streaming Cell 粒度调整
- 最初的目标:1 个 Streaming Cell = 1 个 World Partition Tile
- 实际情况:随着开发推进,需要 更多更小的 Streaming Cell ,以便:
- 更早地将 膝盖高度及以下的小物体 从场景中卸载
- 减少 LOD 0 级别的实例网格数量 ,降低内存与 culling 压力
Streaming Cell 是 World Partition 中控制资产加载/卸载的空间单元。更小的 cell 粒度意味着更精细的加载控制,但也增加了流送管理的复杂度。
植被(Foliage)渲染优化
从 UE4 到 UE5 的植被问题
UE4 时代遗留问题
- 所有植被资产使用 非 Nanite 的 Alpha Mask 面片(masked cards)
- 附带 高开销的风力模拟(wind sims)
- 存在大量 视觉加载(visual popping/loading)问题
- 与 Virtual Shadow Maps(VSM) 完全不兼容,性能极差
初次尝试:直接将面片转为 Nanite Alpha Mask
- 当 Nanite 开始支持植被后,团队第一时间尝试将所有植被转为 Nanite + Alpha Mask
- 视觉加载问题修复了 ,但 渲染耗时完全没有改善
- 本质上只是把 VSM 中糟糕的非 Nanite 耗时 转移成了 糟糕的 Nanite 光栅化耗时 ——开销只是换了个地方
核心优化策略:Masked → Opaque
关键决策
- 最终团队决定 尽一切可能把植被材质从 Masked 转为 Opaque(不透明)
- 具体做法是将原本依赖 alpha 遮罩的叶片面片,改为 实际建模出不透明的几何形状
- 这一步 至关重要 ,因为它大幅减少了:
- 逐像素操作(per-pixel operations) 的数量
- Nanite 光栅化耗时
- VSM 阴影深度 Pass 耗时
风力动画(Wind)处理
- 仍然保留了风力效果,但做了大幅调整
- 树木使用 VATS(Vertex Animation Texture Sheets) + WPO(World Position Offset)
- 但发现 VATS 存在明显局限:
- 顶点数量受限 ——不能有太多顶点
- 内存开销昂贵
- 虽然最终发售版本仍使用了 VATS,但团队计划 后续重新评估 ,特别是配合 UE5 新推出的 动态植被(Dynamic Foliage) 系统
WPO 距离裁剪
- 即使在使用 WPO 的情况下,团队也 非常激进地按距离禁用 WPO
- 有时候禁用距离被拉得 非常近 ,以确保光栅化耗时可控
- 大量 地面杂物(clutter) 如三叶草、小花等 完全移除了 WPO ,甚至完全去掉
特殊情况:无法转为 Opaque 的资产
典型案例:掩护草(Cover Grass)
- 掩护草需要在玩家进入时有 抖动淡出效果(dithering effect) ,因此 必须保留 Masked 材质
- 针对这种无法避免的 Masked 资产,团队采取以下措施:
- 尽可能以不透明方式建模 ——减少 overdraw
- 严格限制顶点数量 ——每个草丛大约 3,000 顶点
- 保持稀疏分布(sparse placement) ——帮助 Nanite 以最优方式光栅化
优化前后性能对比
场景 1:Paradise 外景(Xbox Series S,30 Hz 模式)
| 指标 | Alpha 阶段(优化前) | 最终版本(优化后) |
|---|---|---|
| 总帧时间 | ~46.5 ms | ~30 ms |
| Nanite Vis Buffer | ~4–5 ms | ~2.5 ms |
| Base Pass | ~5 ms | ~3.5 ms |
| Shadow Depth Pass | ~13 ms | ~8 ms |
| Masked 材质光栅化 | ~1.5 ms(单个材质) | 几乎消除 |
额外优化细节:
- Nanite 的 硬件与软件光栅化 从 重叠执行 改为 顺序执行 ,释放出 GPU 时间线空间
- 然后将释放出的空间与 光追场景更新(Ray Tracing Scene Update) 重叠执行,进一步节省时间
- VSM 开销下降也部分得益于 局部光源切换到光追阴影 (后续章节详述)
场景 2:Emerald Stair(Xbox Series S,30 Hz 模式)
| 指标 | Alpha 阶段(优化前) | 最终版本(优化后) |
|---|---|---|
| 总帧时间 | ~53 ms | ~28 ms |
| Nanite Vis Buffer | ~4–5 ms | ~2 ms |
| Base Pass | ~5 ms | ~3.5 ms |
| Shadow Depth Pass | ~20 ms | ~3 ms |
- 该场景中的 红色植被 在优化前仅阴影深度 Pass 就消耗了 ~6 ms
- 优化后阴影深度 Pass 从 20 ms 暴降至 3 ms,降幅 85%
- 最终甚至出现了 ~2 ms 的 Graphics Queue 空闲等待 ,说明 GPU 主管线已经在等待异步 Pass 完成,瓶颈不再是主管线
关键经验总结
| 优化手段 | 效果 |
|---|---|
| Masked → Opaque | 最关键 ,大幅降低光栅化 & 阴影开销 |
| 激进禁用远距离 WPO | 显著降低 Nanite 光栅化耗时 |
| 移除地面杂物的 WPO / 动画 | 释放大量 GPU 预算 |
| Masked 资产限制顶点数 + 稀疏放置 | 让无法避免的 Masked 资产尽可能高效 |
| 调整 Nanite 硬件/软件光栅化执行顺序 | 创造与其他 Pass 重叠执行的机会 |
Lumen 全局光照系统深入分析
为什么选择 Lumen
UE5 之前的 GI 方案评估
- 团队在 UE5 之前评估了多种 全局光照(GI) 方案,发现都存在不足:
- 质量不达标
- 需要大量烘焙时间
- 引擎集成成本巨大 ,拖慢开发节奏
- 很多方案 三个问题同时存在
Lumen 是迁移到 UE5 的首要驱动力
- Nanite 虽然效果亮眼且不可或缺,但如果只为 Nanite,团队 可能可以留在 UE4
- Lumen 才是真正促使迁移到 UE5 的决定性因素
- 最终证明 Lumen 表现出色:
- 达成了所有光照设计目标
- 整个游戏 仅有 2 名灯光师 就完成了全部工作
- 核心原因:无需烘焙,所见即所得(WYSIWYG) ,极大提升迭代速度
硬件光追 vs 软件 Lumen
选择硬件光追的核心原因
- 地形(Landscape)光照反弹 :硬件光追能产生更自然的 光线弹射(light bounce) ,软件 Lumen 在地形反弹上明显不足
- 更高精度、更少漏光(light bleeding)
- 意外收获 :后期引入 光追阴影(Ray Traced Shadows) 时,硬件光追 Lumen 使过渡变得更容易
各平台异步计算优化
| 平台 | 光追队列 | 节省开销 | 备注 |
|---|---|---|---|
| Xbox | 完全运行在 Async Compute 上 | 约 2–4 ms(Series S 测试场景) | 大量使用 Inline Ray Tracing ,节省性能与内存,无需单独的光追 Shader |
| PC | 使用 Graphics Queue | — | 原因:UE 的 D3D12 版本对 Inline Ray Tracing 支持不完整 ;但软件 Lumen 模式下仍使用 Async |
画质对比
- 演讲展示了三组软件 vs 硬件 Lumen 对比场景:
- Obsidian Temple :硬件版本光照反弹更饱满
- Shatterscarp :桥洞下方光照弹射质量明显更好
- Emerald Stair 沼泽 :硬件光追整体光照准确性更高
Emissive(自发光)的巧妙应用
"发光蘑菇"系统
- 团队大量使用 自发光资产(emissive meshes) ——灯光师昵称它们为 "Emissive Mushies(发光蘑菇)"
- 在许多场景中,自发光资产 直接替代了传统灯光 ,节省灯光放置工作量
核心技巧:Ray Tracing Material Node 分离
- 使用 Ray Tracing 材质节点 ,为 光栅化场景 和 Lumen 光追场景 分别设定不同的 emissive 值:
- 光栅化场景(屏幕显示):emissive 强度约 1
- Lumen 光追场景:emissive 强度设为 5–10
- 效果:大幅增强间接光照贡献,同时避免屏幕过曝(blowout)
隐藏 Emissive Mesh Cards
- 团队仍然大量使用 隐藏的 emissive mesh cards 来补充光照
- 性能影响极小,仅占用 Surface Cache 空间
- 但需要 修改引擎 ,将它们从反射 Pass 中剔除:
- 硬件光追反射 :简单——调整 Ray Tracing Mask ,新增一个标志位即可
- 软件 Lumen 反射 :更复杂——需要为 Mesh Distance Fields 添加额外的剔除 Pass
补光手段
- 当 Lumen 场景中噪声过多或高光缺失时,会退回使用 不投射阴影的补光灯(non-shadow casting fill lights) 作为最终手段
Lumen 面临的挑战
1. Pass 数量极多且调试困难
- Lumen 内部的 渲染 Pass 非常多 ,且硬件与软件两套管线存在大量 分歧路径
- 同时维护两套管线 工作量极大
- 调试极其困难 :
- Lumen 使用大量 间接调度(indirection) ——一个 Pass 生成 工作列表(worklists) ,另一个 Pass 消费
- 这导致 GPU 线程与屏幕像素之间的关联被打断 ,无法简单地回溯某个像素的计算来源
- 排查颜色错误或噪声问题变得非常困难
2. 内存压力(尤其是 Series S)
- Lumen 使用大量 Render Targets 和工作缓冲区,显著增加显存占用
- Xbox Series S 内存问题尤为严重
- 关键妥协:在主机上完全放弃 Mesh Distance Fields(MDF) ,仅保留光追 BVH
- 这带来了连锁负面影响:
- 反射系统 在软件模式下受损
- VFX 无法使用 Distance Field 节点 (如距离场碰撞等)
- 结果:团队持续在 性能、质量和内存 三者之间反复权衡
- 这带来了连锁负面影响:
3. 时间积累效果与游戏设计冲突
- Lumen 是 时间累积(temporally accumulated) 的效果,需要多帧稳定才能收敛
- 但 Avowed 有 时间跳跃(Time of Day jumps) ——例如从白天切到夜间营地
- 解决方案:切换时 完全丢弃整个 Lumen 场景 ,在 黑屏淡入淡出(fade) 期间尽快重建
- 闪烁灯光(flickering lights)本身 时间稳定性差 ,会引入噪声
- 大量明亮 VFX 会干扰 Lumen 的屏幕空间效果
Surface Cache 管理问题
大型网格体浪费缓存
- 项目大量使用 巨型单体网格(large monolithic meshes) 以营造宏大感
- 问题:一座大山可能只有一小部分接触到游玩空间,却 占据了约五分之一的 Surface Cache
- 这类浪费 往往难以及时发现 ,直到引发页面频繁驱逐(page eviction)和噪声问题时才暴露
水密网格的背面问题
- 许多网格是 水密(watertight) 的,即有背面几何
- 背面会生成 额外的 Mesh Cards ,进一步浪费 Surface Cache 空间
- 最终只能靠 手动逐个检查和移除 ,耗时费力
Mesh Card 数量优化
- 默认情况下 Lumen 为每个物体生成约 12 个 Mesh Cards
- 对于简单物体(如一面墙),1–2 个就够了
- 团队尝试减少 Mesh Card 数量,但有时会导致 只保留了背面的 Card
- 最终解决方案:添加了一个 偏向修改器(bias mod) ,强制优先选择正面
Lumen 参数调优陷阱
CVar 和后处理参数的潜在风险
- Lumen 有 大量 CVar 可调 ,外加上层的 Post Process 设置
- 并非所有参数调整都是安全的,团队发现了以下问题:
- 内存浪费 :某些本应节省内存的设置反而浪费内存
- NaN 传播 :某些设置组合会在渲染管线中 产生并传播 NaN 值
- GPU 内存踩踏 :最严重的情况下,某些设置会导致 写越 LDS(Local Data Store)边界 ,造成内存踩踏
典型案例:Radiance Cache Screen Probes 内存浪费
- 在 Xbox 默认设置( Screen Probe 分辨率设为 96 )下:
- 纹理按 完整正方形 尺寸分配
- 但实际索引使用 对数映射(logarithmic indexing) ,非 2 的幂次的部分会被钳制到最小值
- 结果:约三分之一的纹理空间被浪费
- 修复方式:修改纹理创建逻辑,按实际需要分配
Lumen 反射系统
异步计算迁移
- UE 5.3 默认将 Lumen 反射放在 Graphics Queue 上
- 团队发现迁移到 Async Queue 非常简单,强烈推荐执行此操作
Hit Lighting(命中点光照)
- PC 端 成功添加了 Hit Lighting
- Xbox 端 未能实现
- 实际影响不大,因为绝大多数光追命中都落在 Surface Cache 中,Hit Lighting 仅影响 动态物体上的缺失光照
艺术家理解困难
- Lumen 反射中 何种材质会触发反射追踪 对美术来说非常不直观
- 例如:"粗糙度低于 0.4 的表面会触发追踪"——习惯用 Photoshop 的美术难以理解这意味着什么
单层水面(Single Layer Water)反射优化
默认行为的性能问题
- UE 5.3 默认的单层水面反射是 全屏 Pass :
- 先做一次 全屏 Screen Space Reflection(SSR)
- SSR 未命中的部分再做一次 全屏硬件光追反射
- 画质好,但 性能开销极大
水下反射表面的双重开销
- 如果 单层水面下方 存在反射表面(例如水下的反光冰面),系统会 对反射追踪支付两次费用
- 团队在 Bingham's Domain 地图中发现了此问题
- 修复方式:将 单层水面深度缓冲(depth buffer) 传递到反射 Pass,通过 深度测试 剔除水下反射表面
水面反射的默认"完美镜面"陷阱
- 单层水面反射 默认总是完美镜面反射
- 美术在水面上绘制泥垢纹理试图降低反射强度(例如沼泽场景),但 底层仍会执行完整的 Lumen 反射追踪
- 修复方式:添加了一个 最大粗糙度追踪阈值(max roughness trace) 修改,并绑定到 Scalability 设置
软件 Lumen 无法使用(因丢弃了 Distance Fields)
- 软件 Lumen 反射本来是更经济的替代方案
- 但由于前述内存限制,团队在主机端 丢弃了 Mesh Distance Fields ,导致软件反射不可用
最终混合方案
- 团队开发了一个 混合反射方案(Hybrid Approach) :
- 保持 全分辨率 SSR
- Lumen 硬件光追反射改为 降采样执行(downsampled) ,以四像素为单位写出 mask
- SSR 未覆盖的区域,填入 低分辨率 Lumen 反射
- 效果:
- 在 静水或平坦水面 上几乎看不出差异
- 在 粗糙表面 上也几乎不明显
- Xbox Series S 上某场景节省了约 0.8 ms ,是一个可观的胜利
阴影系统详解
阴影技术组合概览
Avowed 使用了 三种阴影技术 的组合:
| 技术 | 用途 |
|---|---|
| Virtual Shadow Maps(VSM) | 方向光(Directional Light) 阴影 |
| 光追阴影(Ray Traced Shadows) | 局部光源(Local Lights) 阴影(在启用光追的模式下) |
| Contact Shadows(接触阴影) | 地面小物件细节阴影 + 对话场景光照 |
核心事实 :阴影是整个项目中 性能斗争最激烈的部分 ,甚至在引入光追阴影(实际上反而 节省了 性能)之前就已如此。阴影系统包含了 最多的引擎修改与新增代码 。
Virtual Shadow Maps(VSM)
基本评价
- 相比传统 Cascade Shadow Maps ,VSM 毫无疑问更好
- 搭配 Nanite 使用时 几乎是必选项 :
- VSM 依赖 Nanite 才能获得合理性能
- 反过来,使用 Nanite 时 也应该使用 VSM (除非走全光追阴影路线)
- 对 方向光 效果极佳,支持 超远距离阴影 ,完美服务于开阔场景的远景需求
性能痛点
即便使用了 Nanite + VSM 组合,性能仍然存在挑战:
- 非 Nanite 资产 和 蒙皮网格(skinned meshes) 在 VSM 中开销很大
- 逐像素运算类材质 对 VSM 性能打击严重:
- Masked 材质
- WPO(World Position Offset)
- Pixel Depth Offset
- 这也是团队 极力消除 Masked 材质 和 尽早关闭 WPO 的另一个重要原因
- Shadow Penumbra(阴影半影/软阴影) 有额外开销——需要对阴影贴图 多次采样 以获得柔和阴影边缘
关键优化修改(简单但高效)
1. 按距离减少阴影采样射线数
- 做法极其简单,大约 一行代码 :
- 取 场景深度(scene depth) ,对其取 log₁₀ ,据此调整采样数
- Xbox Series S 在测试场景中节省约 0.5 ms
2. Nanite 实例级阴影剔除(按像素阈值)
- 在 Nanite 实例 Culling 阶段,对 低于特定像素阈值 的实例 直接剔除其阴影投射
- 测试使用 24 像素 作为剔除阈值
- 视觉差异 几乎不可察觉 (仅在远处柱子等处可能看出微小差别)
- Xbox Series S 在测试场景中节省 超过 3 ms ——仅用 1–2 行代码
- 这是 投入产出比极高 的优化
3. 蒙皮网格使用光追替代 VSM 光栅化
- 由于光追管线的负载通常 远低于 光栅化管线
- 将蒙皮网格的阴影从 VSM 光栅化切换到 光追 ,在特定场景中节省约 1 ms
- 画质差异 几乎看不出
4. 调整光源半径以降低投影开销
- 项目后期新增了一个设置项,可以 调整 VSM 的光源半径(light source radius) 以降低投影计算成本
- 用于 Xbox Series S 、 Series X 60 Hz 模式 以及 低优先级 NPC
光追阴影(Ray Traced Shadows)
为什么探索光追阴影
VSM 局部光的核心问题:失效重建(Invalidation)
- 当 动态物体穿过局部光源 时,VSM 会 失效(invalidate) 并 重新渲染所有相关阴影贴图
- 开销 极其高昂
- UE 提供了 Static Separate(静态分离) 机制来缓解,但会 翻倍内存占用 ——主机平台无法承受
光追阴影的天然优势
- 仅追踪屏幕可见像素 ——不追踪被遮挡或屏幕外的内容
- 软阴影/半影免费获得 ——无需额外多次采样
- 无额外 Pass 成本 ——因为已经在使用硬件 Lumen,光追场景已经构建完毕,直接复用
- 灵活的部分阴影方案 :
- 可以让某些物体 在 VSM 中投射阴影但不在光追中投射 ,或反之
- 典型用例:洞穴顶部 可以接收阴影但不在局部光上投射阴影
- 树木 也经常这样处理以节省性能
实现时间线与使用范围
- 实现时间较晚 :在 5.3 升级之后 ,距离发售约 一年左右
- 所有启用光追的平台上,所有局部光源均使用光追阴影
自定义光追阴影 Pass
为什么不用 UE 内置光追阴影
UE 5.3 内置的光追阴影方案 性能极差 :
- 每个光源使用 一个全屏 Dispatch + 一个全屏 Render Target
- 8 个光源 = 8 次全屏 Pass + 8 个 Render Target ,再加上各自的去噪
- 完全无法承受
自定义 Pass 的设计
- 设计思路类似 VSM 的 One Pass Projection
- 使用 Clustered Light Grid → 追踪阴影 → 写回到打包的阴影遮罩(Packed Shadow Mask)
- 该方案 先于 MegaLights 出现,且 MegaLights 在 5.3 时仍为实验性功能
两种模式:
| 模式 | 适用平台 | 采样策略 | 特点 |
|---|---|---|---|
| 全屏 Pass | 高端 PC | 每光源 ≥ 1 sample/pixel | 最高质量 |
| 棋盘格模式(Checkerboard) | 主机 & 低配 | 渲染棋盘格 → 上采样重建 → 对低置信区域重新追踪 | 实际 < 1 sample/pixel/light |
- 默认支持每像素 最多 8 个光源 ——这也是给灯光师的限制("别放太多灯")
- Xbox Series S 上棋盘格模式平均节省约 1–1.5 ms
体积雾阴影适配
- UE 内置的体积雾使用常规光追 Shader,但团队已经移除了那些 Shader(全部改为 Inline)
- 因此需要 为体积雾编写 Inline 版本的光追阴影
- 巧妙优化:重构体积雾函数,使阴影计算 提前启动 ,然后 隐藏在 Async Lumen 后面
- 在没有方向光的室内关卡(如洞穴)中,Async Lumen 运行时间较长,体积雾光追阴影 完全免费
性能对比数据
场景 1:Fior(Xbox Series S,30 Hz)
| 状态 | 总帧时间 | VSM Shadow Depth | 光追阴影 | VSM Projection |
|---|---|---|---|---|
| Alpha 版本(全 VSM) | ~40 ms | ~11 ms | — | ~8 ms |
| 最终版本(光追替代局部光 VSM) | ~25 ms | ~3 ms | ~3 ms | 极小 |
| 假设关闭光追、仅用 VSM | ~29 ms | ~9.5 ms | — | ~2 ms |
| 假设使用 UE 内置光追阴影 | ~34 ms | — | ~18 ms(含去噪) | — |
自定义光追 Pass 相比 UE 内置方案节省约 15 ms ,编写完全值得。
场景 2:Nakukubell(Xbox Series S,30 Hz)
| 状态 | 总帧时间 | VSM Shadow Depth | 光追阴影 | 体积雾阴影 |
|---|---|---|---|---|
| Alpha 版本 | ~37.5 ms | ~13 ms | — | 顺序执行,有开销 |
| 最终版本 | ~23 ms | 0(无方向光) | ~3.5 ms | ~2 ms(免费,隐藏在 Async Lumen 后) |
| 假设关闭光追、仅用 VSM | ~25.5 ms | ~9.3 ms | — | — |
核心规律 :场景中 动态物体和角色越多 ,光追阴影相比 VSM 节省越多
光追管线的其他优化与注意事项
Async 优化
- UE 5.3 中并非所有光追 Pass 都在 Async Compute 上运行,部分仍在 Graphics Queue
- 团队 将所有光追 Pass 移到 Async ,实施简单且效果良好
光追场景更新(RT Scene Update)时序调整
- UE 默认将光追场景更新安排在 Base Pass 期间
- 团队将其 提前到 PreZ 和 Nanite Vis Buffer 阶段 ,节省了时间
地形(Landscape)每帧重建问题
- UE 默认行为:地形 每帧都重建光追加速结构 ,以保持与 Landscape Streaming 同步
- 团队 禁止了这一行为
- 在极端流送性能下降时可能导致 地形与光追不同步 ,但实际上 没有人注意到或报告过问题
内存优势
| 方案 | Xbox Series S 峰值内存 |
|---|---|
| 光追(BVH/BLAS) | ~300 MB |
| Distance Fields(估算) | ~500–600 MB |
- 光追方案节省约 200–300 MB 内存
- 还 避免了 加载新 World Partition 流送单元时 Distance Field 更新引起的 GPU 大尖峰(spikes)
光追的局限:CPU 成本
- 光追在 UE 5.3 中有 较高的 CPU 开销 ,主要来自 收集光追场景(collecting the RT scene)
- 这是 Xbox Series X 60 Hz 模式 和 Series S 上 无法使用光追 的根本原因
- CPU 时间已经吃紧,光追场景收集会将其推过预算上限
- 因此这些模式退回 全 VSM + 软件 Lumen
Virtual Textures(虚拟纹理)
使用范围与收益
- 游戏中 几乎所有纹理 都使用了 Virtual Texture 系统
- 例外情况 :
- VFX 纹理 ——需要即时可用,无法等待反馈加载
- RVT(Runtime Virtual Texture) 纹理——同理
- 核心收益:
- 在更高分辨率下使用 更多纹理
- 降低并稳定内存占用 ——对主机平台尤为重要
性能代价:纹理寻址瓶颈
Texture Address Bound 问题
- Virtual Textures 导致 几乎所有使用它的地方都变成纹理寻址瓶颈(texture address bound)
- 无法从 顶点/像素着色器预缓存(vertex pixel shader pre-caching) 中获益
- 纹理寻址压力 非常大
纹理池管理(Pool Management)
池隔离策略
- 团队发现即使纹理池 没有超额订阅(over-subscribed) ,环境纹理也会 踢掉角色纹理
- 这导致从 游戏画面切换到对话场景 时出现明显的 纹理流送跳变
- 解决方案——按用途隔离纹理池 :
| 纹理类型 | 压缩格式 | 分辨率 |
|---|---|---|
| 角色纹理 | BC7 | 1K |
| 环境纹理 | DXT1 | 2K |
- 未来希望能找到 在同一池内仍能做优先级隔离 的方案
池数量限制与陷阱
- 默认上限 15 个池 ,超出会 硬崩溃(hard crash)
- 可以提升到 31 个 ,但会 翻倍管理开销 ——在内存受限的主机上不理想
- 非常容易意外创建不需要的池 :
- 典型案例:美术人员连接了引擎默认的 1×1 RGBA8 法线贴图 ,导致系统创建一个 不需要的 RGBA 池
- 解决方案:创建一个 2×2 的替代纹理 ,使其可以被 块压缩(block compress) ,归入已有的目标池
材质分层系统的竞态条件 Bug
- 在 Material Layering(材质分层) 系统中发现一个严重问题:
- 如果美术在 Base Material Function 中使用了某个纹理,并在分层系统中依赖它
- 该纹理 不会被正确添加到 材质加载前需要预缓存的 纹理数组 中
- 导致 纹理加载与材质加载之间的竞态条件(race condition)
- 如果材质先加载完成 → 无效的 Virtual Texture 绑定(invalid VT binding) → 持续到材质被重新加载
- 最常见表现: 角色法线贴图丢失 ——因为法线纹理是最容易被错过的
总结与最佳实践
Nanite 最佳实践
- 偏好更多几何体 + 更简单材质 ——Nanite 更擅长处理高几何复杂度,而非高材质复杂度
- 尽一切可能避免逐像素评估开销 :
- 消除 Masked 材质
- 关闭 Pixel Depth Offset(PDO)
- 尽早禁用 WPO(World Position Offset)
- 减少材质变体(material variances) :
- Nanite 倾向于按 材质批次 绘制——材质种类越少,Draw Call 越少
- 尽可能通过 Custom Primitive Data(CPD) 实现材质变化,而非创建新材质
- 配合 World Partition 控制加载内容——Nanite 光栅化虽快,但 大量无意义的小物件仍会拖慢速度
Lumen 最佳实践
- 密切关注 Surface Cache 中的内容 ——非常容易浪费空间
- 谨慎调整 CVars :
- 需要了解每个 CVar 的具体作用
- 逐个调整 ,不要一次性批量修改——否则容易陷入无法排查的问题
- 从硬件光追开始,再降级到软件 ,比反方向容易得多:
- 如果项目刚启动,建议 默认选择硬件光追 Lumen
- 后续需要时降级到软件模式远比从软件升级到硬件简单
阴影最佳实践
- 不要固守单一阴影方案 ——混合使用效果最好:
- VSM → 方向光(表现优秀)
- 光追阴影 → 局部光(点光源、聚光灯、矩形光)
- Contact Shadows → 不要忽视它们!团队内部曾对其有"昂贵"的错误印象,但实际上 比其他方案便宜得多
Virtual Textures 最佳实践
- Virtual Textures 解放了纹理使用自由度,但并非零成本
- 时刻关注纹理池状态
通用工程实践建议
基准硬件选择
- 尽早确定基准测试硬件 ,团队强烈推荐 使用主机
- 硬件固定,无其他后台进程干扰
- 不受显卡驱动更新影响
- Avowed 选择 Xbox Series S 作为基准——效果非常好
- 在多平台发售时,找到 Series X 上与 Series S 性能表现对等的设置 ,以此为基线逐步调整
开发期间禁用动态分辨率
- 开发阶段必须关闭 Dynamic Resolution
- 动态分辨率应仅用于 应对发售版的瞬时帧率尖峰 ,而非掩盖性能问题
- 开着它相当于 把头埋进沙子 ——完全不知道 GPU 真实负载状态
优化时关闭异步工作
- 在分析和优化特定渲染 Pass 时, 务必关闭 Async Compute
- 异步重叠会 扭曲计时结果 ——你可能以为取得了进展,实际只是工作重叠方式变了
持续性能监控
- 每周性能例会 :
- 让 QA 人员玩游戏,工程师旁观并记录
- 即使不立即修复,也至少 了解当前性能状况 和变化趋势
- 自动化回放系统(Autoplayer) :
- 强烈推荐——帮助跟踪优化进展
- 覆盖手动游玩无法触及的场景区域
保留并定期回顾旧的 GPU 抓帧
- 在开发过程中保留大量 PIX 等工具的 GPU 抓帧
- 每次 引擎升级 或 大量内容提交 后,对比旧抓帧检查 性能回退(regression)
- 实际案例:
- 从 FSR 2 升级到 FSR 3 后,出现了 约 1–3 ms 的性能回退
- 长时间未被发现,直到回顾旧抓帧才注意到
- 对比后发现 FSR 3 相对 FSR 2 画质提升不大 ,于是 回退到 FSR 2 ,直接省下这部分开销