idTech8 全局光照方案:Fast as Hell
演讲者与项目背景
- 演讲者: Thiago Souza ,id Software 渲染技术总监,拥有超过 20 年游戏行业经验
- 参与过的知名项目:Far Cry、Crysis 三部曲、Doom 2016、Doom Eternal、Wolfenstein 2: The New Colossus 等
- 本次演讲聚焦于为 idTech8 引擎 开发的 全局光照(Global Illumination) 方案
- 该方案已在两个大型项目中落地验证:
- 《Indiana Jones》(Machine Games 开发)
- 《Doom: The Dark Ages》(id Software 开发)
- 这两个项目是公司迄今为止最复杂的项目,包含复杂过场动画、高级毛发渲染、更精细的环境与几何细节、更复杂的光照与着色
工作室协作模式
- id Software 和 Machine Games 是相对 规模较小 的工作室(以现代标准衡量),技术团队尤其精简
- 两个工作室之间 共享技术 :
- 毛发渲染技术来自 Indiana Jones 项目
- 全局光照的初始实现由 id Software 提供给 Machine Games
- 本次演讲的内容主要基于 Doom: The Dark Ages 最终发行版本 所使用的方案
idTech8 引擎的核心需求
1. 极致性能表现
- id Software 对性能有着近乎 执念般的追求
- 目标:在 所有平台 上实现 稳定 60fps 或更高
- "稳定"的定义非常严格:无卡顿(stuttering)、无停顿(stalls) ,即 丝般顺滑 的体验
2. 大幅加速迭代速度
- 希望为工作室所有人 大幅提升工作流效率
- 随着项目复杂度持续增长,核心策略是:
- 尽可能消除预处理(Preprocessing) 步骤
- 简化流程 ,减少所需步骤数量
- 降低出错率 ,让工具对用户更友好
3. 支持更大规模的关卡
- 美术和设计部门希望制作 远超以往规模的关卡
- 需要解决的问题:
- 如何在 磁盘占用合理 的前提下扩展
- 如何在消除预处理的同时不增加处理时间
- 同时还要在屏幕上呈现:
- 更多角色、更精细的角色(致敬 90 年代经典 Doom 的风格)
- 更高的几何细节
- 更多动态元素 :可破坏物理、植被动画、骨骼动画等
4. 关卡规模数据
- 所有关卡 玩家可行走表面积 合计约 5,000 平方公里
- 这仅统计了地图上的绿色可行走区域
- 不包括 远景山脉、建筑等 Vista 区域
- 与前作对比:关卡面积达到 4 倍到 10 倍 ,关卡数量 接近翻倍
之前的方案及其痛点:光照贴图(Lightmap)
之前的做法
- 对于 静态不透明几何体 ,过去依赖 路径追踪烘焙的光照贴图(Path-Traced Lightmaps)
- 对于 动态物体和半透明物体 ,无法使用光照贴图,需要各自独立的解决方案
光照贴图的预处理痛点
实现一个完整的 Lightmapper 需要大量预处理步骤:
- 生成唯一的 UV 展开(Unique UV Unwrap)
- 构建各种 数据结构
- 处理 光照贴图 LOD(Lightmap LOD) 等问题
这些正是 idTech8 希望 摆脱 的繁重预处理流程,也是推动他们开发新全局光照方案的核心动机之一。
核心要点总结
| 维度 | 目标 |
|---|---|
| 性能 | 全平台稳定 60fps+,零卡顿 |
| 工作流 | 消除预处理,简化流程,降低出错率 |
| 规模 | 支持 5000km² 级别的超大关卡 |
| 复杂度 | 更多角色、更高几何细节、更多动态元素 |
| 旧方案痛点 | 光照贴图需要大量预处理(UV 展开、数据结构、LOD 等) |
光照贴图烘焙流水线详解与痛点分析
光照贴图烘焙的优化策略
剔除与 LOD 策略
- 遮挡剔除(Occlusion Culling) :如果某个光照贴图区域从玩家可行走区域看不到(被遮挡),则 跳过该区域的烘焙 ,避免浪费计算资源
- 距离自适应精度 :距离玩家行走区域越远的光照贴图实例,使用 越低的计算频率/精度 ——细节只需保留在相机附近
分布式烘焙架构
- 将所有烘焙工作负载 分配到工作室内的多台机器 上并行处理
- 每台机器尝试处理 相近数量的 Texel 数据 ,实现负载均衡
- 光照贴图被分割为多个 Tile(瓦片) ,每个 Tile 再分配到 多个线程 上执行
每个 Tile 的处理流程
- 光栅化(Rasterize) :对每个 Tile 中的 Texel,光栅化与该 Tile 重叠的三角形
- 计算属性 :确定每个 Texel 的 世界空间位置(World Space Position) 、 法线(Normal) 、 反照率(Albedo) 等属性
- 路径追踪(Path Tracing) :启动路径追踪过程,计算全局光照
- 更新辅助结构 :更新所需的数据结构,包括 辐照度缓存(Radiance Cache) ,用于在所有 Texel 数据间 共享计算开销
- 编码存储 :最终将 GI 结果使用 球谐函数编码(Spherical Harmonics Encoding) 存储
光照贴图的存储策略
双版本存储
烘焙完成后,会在网络上存储 两个版本 的光照贴图:
| 版本 | 说明 |
|---|---|
| 块压缩版(Block Compressed) | 用于当前平台的运行时使用 |
| 非块压缩版(Uncompressed) | 作为无损源数据保留 |
- 两个版本都会进一步使用 Kraken 压缩 来减少磁盘占用
为什么要存两个版本?
- 核心原因:避免累积块压缩伪影(Block Compression Artifacts)
- 当移植到不同平台时(如 Nintendo Switch 使用 ASTC 块压缩格式 ),如果从已压缩版本再次压缩,伪影会叠加恶化
- 保留无损源数据还允许团队 实验不同编码方式 ,而 无需重新烘焙
光照贴图的重新烘焙触发条件
以下任何一项变更都会 触发整张地图的重新烘焙 :
- 太阳位置 改变
- 大气散射(Atmospheric Scattering) 设置改变
- 美术在某个材质上启用了 自发光(Emissive) 属性,而该材质被 实例化(Instanced)到整个地图
- 某个 实例化几何体的拓扑结构 发生变化,而该几何体同样遍布整个地图
总结:光照贴图与场景数据之间存在 广泛的依赖关系 ,任何微小改动都可能导致全图重烘焙。
光照贴图的固有缺陷
低 Texel 密度导致的视觉问题
由于 Texel 密度本身不高,会出现一系列经典问题:
- 漏光(Light Leaking) :光线穿透薄墙等不应透光的区域
- 条带伪影(Banding) :渐变区域出现明显的色带
- 块压缩伪影(Block Compression Artifacts) :压缩引入的视觉误差
美术层面的 Workaround(变通方案)
- 放置 贴花(Decals) 遮盖伪影
- 使用 厚墙壁 防止漏光(因为 Texel 密度不够,薄墙无法阻挡光线泄漏)
- 其他各种手工修补手段
演讲者评价:团队在优化上花了 大量时间 ,但 远远不够 。
实际烘焙性能数据
烘焙时间
- 较大的地图单次烘焙耗时高达 40 小时
- 不得不将默认分辨率降到约 2×2 像素每平方米
- 关键区域美术总监(RTM)可手动覆盖为更高精度
最终发布版统计数据(Doom: The Dark Ages)
| 指标 | 数据 |
|---|---|
| 单张地图最终质量烘焙时间 | 4 小时 ~ 10 小时以上 |
| 全地图烘焙周转时间 | 约 4 天 |
| 每个关卡磁盘占用 | 约 500 MB 压缩数据 |
动态几何体与半透明物体的问题
独立方案的额外痛点
- 动态几何体和半透明物体需要 独立的解决方案 ,无法复用光照贴图
- 这些方案同样受到 重新烘焙依赖 的困扰:
- 地图上任何改动 → 需要重新烘焙
- 并且 依赖于光照贴图本身 :必须等光照贴图烘焙完成后,才能更新动态物体的间接光照数据
多系统多依赖的连锁问题
- 多个系统 、 多个依赖关系 交织在一起
- 一旦某个环节出错,排查和修复非常痛苦
典型的日常问题
- 美术或设计师经常过来询问:"为什么这个表面看起来很奇怪/黑色/不对劲?"
- 典型案例:某些表面显示为 纯黑色 ——通常意味着烘焙数据损坏或过期
- 这类问题 频繁发生 ,严重干扰制作流程
关键总结
| 痛点类别 | 具体表现 |
|---|---|
| 烘焙耗时 | 大地图 40 小时,全图 4 天周转 |
| 脆弱的依赖链 | 任何微小改动触发全图重烘焙 |
| 视觉质量 | 低 Texel 密度导致漏光、条带、压缩伪影 |
| 磁盘占用 | 每关卡 500MB |
| 多系统耦合 | 动态物体方案依赖光照贴图,连锁出错 |
| 人力成本 | 美术需要大量手工修补,频繁排查黑面等问题 |
这些痛点最终推动了 idTech8 完全放弃光照贴图 ,转向全新的实时全局光照方案。
光照贴图方案的可扩展性危机与光线追踪转型
分布式烘焙环境中的实际问题
在分布式烘焙流水线中,还会遭遇各种 难以定位的实际工程问题 :
- 代码 Bug :例如某个网格拓扑发生变化,是代码引入的缺陷还是其他原因?
- NaN(非数值)问题 :在分布式环境中跨多台机器排查 NaN 错误非常困难
- 烘焙任务异常 :某台云端机器上的光照贴图计算耗时异常或未完成
- 环境干扰 :演讲者最喜欢的案例是 —— Windows Update 不知为何自动启动 ,导致烘焙被打断
这些问题在分布式环境中极难定位和复现,极大消耗工程团队的精力。
如果继续沿用光照贴图方案的预估数据
假设条件
- 关卡面积增长到 4 倍~10 倍
- 关卡数量 接近翻倍
预估数据
| 指标 | 最佳情况 | 最坏情况 |
|---|---|---|
| 单关卡烘焙时间 | 最高 40 小时 | 最高 100 小时 |
| 单关卡压缩数据量 | 约 2 GB | 约 5 GB |
| 全游戏磁盘占用 | 约 44 GB | 约 110 GB |
| 全地图烘焙周转时间 | 约 1 个月 | 约 2 个月 |
增加机器是否能解决问题?
- 当时已使用约 64 台机器 进行分布式烘焙
- 即使 翻倍机器数量 :
- 迭代速度 不会有质的提升
- 内存和磁盘需求完全不变
- 结论: 完全不可扩展(Not Scalable)
光线追踪的早期探索与经验教训
首次光线追踪实践
- 2021 年 6 月 ,id Software 为 idTech7 发布了首个 光线追踪更新
- 这次经验提供了非常宝贵的认知
核心教训
- 硬件并没有那么快 :能追踪的光线数量有限
- 低端硬件与高端硬件差距巨大 :可达 数量级 的性能差异
- 低端基准线: Xbox Series S 以及 PC 端对应级别的 GPU
- 硬件性能增长有限 :从 2021 年到 2025 年发售 Doom: The Dark Ages,目标主机 完全没有变化 ,是同一代硬件
光线追踪的正确思维方式
- 常见误解 :光线追踪只能用于反射
- 正确理解 :光线追踪本质上是一种 通用的可见性查询工具(Visibility Query Tool)
- 以这种思维看待光线追踪,就能发掘出更多创造性的用法
idTech8 中光线追踪的多种用途
材质查询(Material Queries)
- 典型场景:玩家 射击墙面 时
- 墙面可能由 多达 8 层材质混合(Per-Pixel Blending) 组成
- 通过光线追踪在命中点确定 最近的材质层
- 根据材质类型生成对应的 音效和视觉特效
其他用途
- 粒子碰撞检测(Particle Collisions)
- 流式加载反馈(Streaming Feedback)
- 其他可见性相关的查询场景
实时 GI 的设计哲学
核心策略
鉴于硬件性能有限且短期内不会大幅提升,需要采取以下策略:
- 分摊计算开销(Amortize Computation Costs) :将昂贵的计算分散到多帧
- 解耦计算频率(Decouple Frequency of Computations) :不同部分以不同频率更新
- 这些都是实时渲染中的 常规优化手段(Bread and Butter)
实用性导向
- 研究灵感来源于当时大量的 实用性研究(Practical Research)
- "实用性"的定义:能在 用户级硬件 上运行——即主机和大众 PC,而非仅限高端设备
单帧全局光照流水线概览
整体架构
每一帧的 全局光照(Global Illumination) 处理包含以下步骤(后续逐一展开):
- 构建必要的 数据结构 ,用于将计算 延迟到后续步骤
- 首先构建的是 级联光照网格(Cascaded Light Grids) ——这一结构在 Doom Eternal 的光线追踪更新中已经引入
- 该结构的核心作用:提供一种机制,能够在 世界空间中任意位置 进行光照查询
级联光照网格是整个实时 GI 系统的基础数据结构,后续所有计算都建立在其之上。
级联光照网格(Cascaded Light Grids)详解
核心概念与设计思想
本质定义
- 级联光照网格 是一种 世界空间的索引结构 ,用于回答一个基本问题:在空间中某一点,有哪些光源、贴花(Decals)和反射探针(Reflection Probes)需要处理?
- 精神上类似于 Clustered Binning(簇化分箱) ——演讲者表示非常喜欢这个技术,因为它对 不透明表面、半透明物体 等各种情况都通用
- 关键区别:传统 Clustered Binning 工作在 屏幕空间/视锥空间 ,而级联光照网格工作在 世界空间(World Space)
为什么用世界空间?
- GI 计算需要在屏幕外的空间进行查询(如光线追踪命中屏幕外的点)
- 世界空间结构不依赖于相机视角,更适合全局光照场景
数据结构参数
级联配置
| 参数 | 值 |
|---|---|
| 级联数量 | 8 个 |
| 每个级联的分辨率 | 16 × 16 × 16 |
| 分布方式 | 指数分布(Exponential Distribution) |
| 第一级联范围 | 32 米 ,约 2 立方米/单元格 |
| 后续级联 | 64m → 128m → 256m → …依次翻倍 |
存储内容
- 支持 三种 ID 类型 :
- 光源(Lights)
- 反射探针(Reflection Probes)
- 贴花(Decals)
- 每个单元格、每种类型最多存储 64 个 ID
- 通过 ID 可以索引到对应的 属性列表 ,查找光源位置、颜色等参数
内存消耗
- 总计约 30 MB
- 内存主要被 大量贴花 占据——相机附近可达 30,000 个贴花
- 演讲者半开玩笑称这可能是 吉尼斯世界纪录
- 对于贴花数量较少的项目,内存需求会小很多
GPU 实现细节
整体架构:单次同步 Compute Dispatch
- 实现非常直接:一个同步的 Compute Shader Dispatch
- Dispatch 分辨率 = 网格分辨率 ÷ Thread Group Size(在 Z 分量上编码级联编号)
- 每个线程知道自己正在处理 哪个级联的哪个单元格
三阶段层级化收集(Hierarchical Gathering)
第一阶段:CPU 端粗粒度标记
- 在 CPU 端预先标记 每个 Item 与哪些级联有重叠
- 如果某个 Item 完全不在某个级联内 ,GPU 端直接跳过,不做任何处理
第二阶段:GPU 粗粒度剔除(Coarse Culling)
- 每个线程映射为级联中的一个 世界空间粗粒度单元格
- 粗粒度单元格尺寸 = 级联范围 ÷ Thread Group Size
- 例如第一级联 32m 范围,除以 Thread Group Size 得到粗粒度单元格的立方体尺寸
- 对每个粗粒度单元格,执行 OBB vs AABB 重叠测试 ,收集所有重叠的 Item
- 结果存入 Group Shared Memory 中的 Flat Bit Array 桶
Flat Bit Array 的关键特性
- 参考自 Roberto(2017 年的演讲) 提出的方案
- 本质:一种 紧凑存储 ID 元素 的方式
- 核心优势:输出结果是有序的(Sorted)
排序为什么重要?
- 贴花 有 叠加顺序 :美术放置一个贴花在墙上,再在上面叠放另一个贴花;游戏中玩家射击墙面会在最上层产生弹孔贴花
- 反射探针 也有 优先级排序
- 在 大规模并行环境 中,每个线程完成时间不确定——如果不按固定顺序写入结果,会导致 画面闪烁(Flickering)
- Flat Bit Array 天然保证排序,解决了这个并行一致性问题
存储结构
- 每种 Item 类型一个独立的 Flat Bit Array 桶:
- 光源桶
- 贴花桶
- 环境/反射探针桶
第三阶段:原子化精细填充(Atomic Pooling)
- 在粗粒度收集之后,进行更精细的 原子化填充 操作
- 将粗粒度结果细化到最终的 16×16×16 网格单元格中
流程总结
CPU 端:标记 Item ↔ Cascade 重叠关系
↓
GPU Compute Dispatch(单次同步)
├── 每个线程 → 一个粗粒度世界空间单元格
├── OBB vs AABB 重叠测试
├── 结果写入 Group Shared Memory(Flat Bit Array)
├── 保持排序顺序(避免并行闪烁)
└── 原子化精细填充 → 最终 16³ 网格
↓
运行时查询:给定世界空间位置 → 查找级联 → 读取 Light/Decal/Probe ID 列表
关键设计要点总结
| 要点 | 说明 |
|---|---|
| 世界空间操作 | 不受相机视角限制,适合 GI 的屏幕外查询 |
| 指数级联 | 近处高精度、远处低精度,平衡精度与内存 |
| Flat Bit Array | 紧凑存储 + 天然有序,解决并行写入一致性 |
| 层级化收集 | CPU 预标记 → GPU 粗粒度剔除 → 精细填充,逐步缩小范围 |
| 单次 Dispatch | 实现简洁,仅一个同步 Compute Shader 调用 |
级联辐照度体积与世界辐照度缓存
级联光照网格:最终阶段与性能
第三阶段:精细填充
- 将单元格尺寸映射为 级联范围 ÷ 网格分辨率
- 遍历 Flat Bit Array 桶 中的候选项,再次执行 OBB vs AABB 重叠测试
- 将通过测试的结果写入 最终缓冲区 ,完成整个网格构建
性能数据
| 平台 | 耗时 |
|---|---|
| PC | 约 0.1 ms |
| 主机 | 约 0.2 ms |
- 考虑到处理的数据量,性能相当不错
- 演讲者承认 仍有优化空间 ,但团队判断有"更重要的战场",因此选择继续推进
已知局限性
- 与 Clustered Binning 存在类似问题:
- 远处级联的 单元格在世界空间中更大 → 更多 Item 与其重叠
- 导致 远处的 Overdraw 增加
- 最简单的缓解方式是 提高级联分辨率 ,但团队没有这样做——当前配置已在 内存与性能之间达到最佳平衡点
级联辐照度体积(Cascaded Radiance Volumes)
设计目的
- 光照网格解决了"空间中某点附近有哪些光源/贴花/探针"的问题
- 现在需要解决另一个问题:如何采样世界的辐照度信息?
- 方案:使用 级联辐照度体积 ,以每个单元格的 中心点 作为采样位置,从该位置向世界发射光线
结构参数
| 参数 | 值 |
|---|---|
| 每个级联的分辨率 | 16 × 16 × 16 |
| 级联数量 | 6 个 |
| 分布方式 | 指数分布(Exponential Distribution) |
关键优化:分帧摊还(Amortized Update)
- 不是每帧更新所有数据
- 将计算成本 分摊到多帧 :每帧只更新 一个级联的一个体积
- 这是前面提到的 分摊计算开销 哲学的直接体现
光线追踪策略:仅追踪可见性
- 在此阶段 不做任何光照计算或着色
- 目标是让这一步 尽可能轻量
- 只追踪 可见性射线(Visibility Rays)
每帧射线数量(取决于质量设置)
| 质量等级 | 每采样点射线数 |
|---|---|
| 高 | 64 条 |
| 中 | 32 条 |
| 低 | 16 条 |
射线命中数据的最小化打包
- 每条射线命中后,存储 最小化的打包命中数据 ,用于后续帧中 延迟重建
- 总共 128 位(16 字节) ,包含:
| 字段 | 说明 |
|---|---|
| Shader Binding Table Index | 着色器绑定表索引(材质/着色器变体) |
| Primitive ID | 图元 ID |
| Instance Index | 实例索引 |
| Barycentrics | 重心坐标(用于插值顶点属性) |
| Hit Distance | 命中距离 |
核心思想:延迟计算(Deferred Computation) ——先只记录"光线打到了什么",把"那个表面长什么样、怎么着色"推迟到后续阶段。
世界辐照度缓存(World Radiance Cache)
触发机制
- 每条射线的命中都会 触发一次缓存更新(Cache Update)
- 这是整个 GI 系统的核心数据结构之一
底层结构:空间哈希(Spatial Hashing)
- 基于 Gontier(2020) 提出的 空间哈希 方案
- 选择该方案的原因:极致的简洁性 ——演讲者强调"我喜欢保持简单,没有比一维数组更简单的了"
工作原理
哈希函数输入(N维)→ 哈希值 → 索引到一维数组
- 哈希函数的输入 是从世界中 量化(Quantized) 得到的信息:
- 量化后的位置(Quantized Position)
- 量化后的法线(Quantized Normal)
- 细节层级(Level of Detail)
- 其他可自定义的维度
演讲者特别指出:哈希函数的设计可以很有创意 ——你可以根据需求自由选择输入维度。
具体配置
| 参数 | 值 |
|---|---|
| 存储类型 | 体积数据(Volumetric Data) |
| 量化精度 | 25 cm 立方体 / 单元格 |
| 总单元格数 | 约 100 万个 |
| 内存占用 | 仅 14 MB |
LOD 感知的哈希
- 哈希函数中 包含单元格的 LOD 信息
- 原因:对于 数公里之外的区域 ,不需要保持与近处相同的分辨率
- 远处使用更大的量化步长,自然减少所需的缓存条目
哈希冲突处理
- 哈希冲突不可避免
- 处理方式:线性探测(Linear Probing)
- 如果目标位置已被占用,则检查数组中的 下一个元素
- 如果可用,则 复用该位置 作为新的索引
缓存更新的具体流程
每条射线命中时的操作
射线命中 → 计算哈希 → 原子递增活跃帧单元格计数器 → 写入列表
数据组织方式
- 维护 每个 Dispatch Index 一个计数器和一个列表
- Dispatch Index 本质上对应 着色器变体索引(Shader Variation) ,从 Shader Binding Table 中获取
列表中存储的内容
| 存储位置 | 内容 |
|---|---|
| 列表末尾项 | 单元格哈希索引(Cell Hash Index) |
| 哈希索引位置 | 射线索引(Ray Index) 和 探针 ID(Probe ID) |
设计意图
- 所有这些操作都是为了 将计算延迟到帧的后续阶段
- 存储足够的索引信息,使得后续阶段能够 完整重建 所需的几何和材质数据
- 这种 间接引用链(Indirection Chain) 是整个延迟计算架构的关键:
- 射线命中 → 缓存哈希 → 着色器列表 → 后续着色
设计哲学总结
| 原则 | 体现 |
|---|---|
| 延迟计算 | 光线追踪阶段只记录命中信息,着色推迟 |
| 分帧摊还 | 辐照度体积每帧只更新一个级联 |
| 最小化存储 | 命中数据仅 128 位,缓存仅 14 MB |
| 简洁性 | 空间哈希 = 一维数组 + 哈希函数 |
| LOD 感知 | 远处降低缓存精度,避免浪费 |
世界辐照度缓存着色与最终收集(Final Gather)
世界辐照度缓存更新:着色阶段
触发条件与调度方式
- 当所有命中数据和缓存结构准备就绪后,触发 世界辐照度缓存的着色更新
- 对每个 活跃的缓存条目(Active Cache Entry) 执行着色计算
- 使用 活跃帧单元格计数器(Active Frame Cell Counters) 来设置 间接调度(Indirect Dispatches)
- 对每种支持的着色器执行一次 异步间接调度(Asynchronous Indirect Dispatch)
着色器简洁性
- id Software 的一贯原则:保持简单
- 着色器种类 非常少 ——这极大简化了工程复杂度,让开发"更轻松愉快"
着色流程
- 使用 Ray ID 和 Probe ID 推导出 全局射线索引(Global Ray Index)
- 加载对应的 射线打包命中数据(Ray Hit Packed Data)
- 从 Shader Binding Table Index 确定需要访问的 三角形数据
- 利用 重心坐标(Barycentrics) 计算 命中点的世界空间位置
- 执行 标准的光照/着色循环 ——与 光栅化路径使用完全相同的代码
- 通过 预处理宏(Defines) 排除部分代码段以优化性能
多次反弹的模拟:迭代式方法
- 不追踪更多射线 来模拟多次反弹
- 采用 迭代式过程(Iterative Process) :
- 每帧读取 上一帧的辐照度体积(Previous Frame Irradiance Volumes) 数据
- 渲染的帧数越多 → 等效的 光照反弹次数越多
- 这是一种 时间上的递归 ,避免了单帧内追踪多级光线的高昂开销
更新规模与复用策略
| 参数 | 值 |
|---|---|
| 每帧更新的缓存条目数 | 约 20,000 个 |
| 缓存复用 | 更新后的条目在 多帧内复用 ,帧数取决于质量设置 |
核心思想:避免每帧重复计算 ,已计算的结果尽可能跨帧复用。
辐照度体积更新(Radiance Volumes Update)
更新流程
- 辐照度缓存的第一个用途:更新辐照度体积中的辐照度数据
- 实现为 单次 Compute Dispatch
- 每帧 只更新当前正在处理的体积 :即当前级联(Cascade)和当前本地辐照度体积(Local Irradiance Volume)
Thread Group 设计
| 参数 | 值 | 说明 |
|---|---|---|
| Thread Group Size | 8 × 8 = 64 | 恰好对应每个采样点最多 64 条射线 |
- 从 Probe Index 和 Ray Index 加载所需数据:
- 探针位置(Probe Position) → 即采样位置
- 射线命中距离(Ray Hit Distance)
- 射线方向(Ray Direction)
缓存查找与 Group Shared Memory 优化
- 根据射线命中点计算 空间哈希值
- 索引到 世界辐照度缓存 获取着色结果
- 将结果存入 Group Shared Memory
- 目的:计算一次,多次复用 ——避免在处理每个 Texel 时重复执行相同的缓存查找
- 在后续计算每个 Texel 的辐照度时,直接从共享内存读取
时间连续更新
- 结合 上一帧的数据 进行连续更新(又一处迭代式多帧积累)
存储格式
- 最终结果存储为 探针图像图集(Probe Image Atlas)
- 使用 八面体环境映射(Octahedron Environment Mapping) ——由 Cigolle/Zak Baka(2008) 提出
- 本质:将球面数据映射到 2D 图像 ,极其简洁
- 符合 id Software "越简单越好"的设计哲学
存储内容与结构
- 与 DDGI(Dynamic Diffuse Global Illumination) 非常相似
- 存储两类数据:
- 颜色(Color) :辐照度信息
- 可见性(Visibility) :用于遮挡判断
数据规模与性能
| 参数 | 值 |
|---|---|
| 级联数 | 6 个 |
| 本地辐照度体积数 | 最多 100 个 |
| 最新硬件耗时 | 约 0.08 ms ,几乎可忽略 |
| 全平台表现 | 所有硬件上都运行很快 |
最终收集(Final Gather)
阶段定位
- 此时所有辐照度体积已更新完毕
- 所有缓存已 预热(Warmed Up) 并就绪
- 进入 最终收集(Final Gather) 阶段
关键特征:零着色、零光照
- 在此阶段 不做任何着色和光照计算
- 只索引已有的缓存数据 ——这是前面所有延迟计算、缓存预热的成果体现
实现方式
- Compute Shader 执行
- 分辨率取决于 质量设置 :
| 质量等级 | 分辨率 |
|---|---|
| 低 | 每个轴 1/4 分辨率 |
| 高 | 每个轴 1/2 分辨率 |
射线配置
| 参数 | 值 |
|---|---|
| 每像素射线数 | 1 条 |
| 射线分布 | 余弦加权分布(Cosine Weighted Distribution) |
| 方向变化 | 使用 蓝噪声(Blue Noise) 控制射线方向的采样变化 |
余弦加权分布 :偏向法线方向采样更多射线,符合漫反射表面的物理特性(Lambert 余弦定律),近法线方向贡献最大。
蓝噪声 :一种高频均匀分布的噪声模式,用于采样时产生视觉上更均匀、更少聚集感的随机方向变化,比白噪声在低采样率下产生更好的感知质量。
整体架构回顾:数据流总结
级联辐照度体积(Visibility Rays)
↓
射线命中数据(128-bit packed)
↓
世界辐照度缓存(Spatial Hash)
↓ 着色(与光栅化共享代码)
缓存条目更新(~20K/帧)
↓
辐照度体积更新(Probe Atlas,八面体映射)
↓
最终收集(Final Gather)
→ 索引缓存,零着色
→ 低分辨率,1 ray/pixel,余弦加权 + 蓝噪声
核心设计哲学贯穿始终
- 延迟计算 :先记录命中,后续统一着色
- 分帧摊还 :昂贵计算分散到多帧
- 迭代式多弹 :通过读取上一帧数据模拟多次反弹,无需额外射线
- 缓存复用 :计算一次,跨帧和跨 Texel 反复使用
- 极致简洁 :少量着色器、简单数据结构、统一代码路径
最终收集(Final Gather):缓存回退链、去噪与时间累积
辐照度缓存回退链(Cache Fallback Chain)
当处理每条射线的命中点时,系统按以下 优先级顺序 逐级回退查找辐照度数据:
三级回退机制
| 优先级 | 缓存源 | 使用条件 |
|---|---|---|
| 1. 屏幕空间缓存(First Cache) | 上一帧的结果 | 命中点在 视锥体(Frustum)内 且 未被遮挡(Not Occluded) |
| 2. 世界辐照度缓存(World Radiance Cache) | 空间哈希缓存 | 命中点在 视锥体外 ,或虽在视锥体内但 被遮挡 |
| 3. 辐照度探针(Irradiance Probes) | 最后的兜底方案 | 世界辐照度缓存在该命中位置 没有有效数据 |
核心逻辑:优先使用最精确的数据源,逐级降级到覆盖范围更广但精度更低的缓存。
球谐函数编码(Spherical Harmonics Encoding)
为什么使用球谐函数?
- 处理完成后,需要在最终结果中保留 逐像素的法线信息 和 光照方向性(Directionality)
- 球谐函数天然支持编码方向性信息
存储格式
- 每像素存储 一个辐照度探针 的等效数据
- 使用 3 张 RGBA 16F 纹理 (对应 RGB 三个颜色通道的球谐系数)
| 通道 | 内容 |
|---|---|
| R | L0 分量(环境光/均值) |
| G, B, A | L1 分量(方向性信息,三个方向轴) |
- 之所以需要 3 张纹理 :每个颜色分量(R、G、B)各有一组完整的球谐系数
去噪流水线(Denoising Pipeline)
问题根源
- 射线数量有限(每采样点最多 64 条),原始结果 非常嘈杂("有点土豆")
- 需要 放大/补全(Upscale) 并 共享邻域结果 来弥补采样不足
演讲者直言:"去噪(Denoise)只是'复用邻域和时空数据'的花哨说法。"
第一步:空间去噪(Spatial Denoise)
实现方式:可分离双边高斯滤波(Separable Bilateral Gaussian)
- 在 Final Gather 分辨率 下执行 Compute Dispatch
- 预加载所有数据到 Group Shared Memory :
- 辐照度(Radiance)
- 深度(Depth)
- 法线(Normals)
- 最近命中(Closest Hit)
- 数据尽可能 压缩打包 ,使用 FP16 提升性能
滤波核权重
不仅考虑高斯距离权重,还加入:
- 法线权重 :法线差异大的邻域降低权重
- 深度权重 :深度差异大的邻域降低权重
参考 Peter-Pike Sloan(2007) 提出的双边缩放(Bilateral Scale)方案。演讲者认为这已成为 行业标准做法 。
第二步:双边上采样(Bilateral Upscale)
- 目标:将去噪后的低分辨率结果 上采样到全屏分辨率
- 因为运行在全屏分辨率,必须 尽可能精简 :
- 仅使用 4 个采样点 ,基于 泊松分布(Poisson Distribution)
- 同样按 深度和法线 加权
第三步:时间累积(Temporal Accumulation)
问题
- 单帧空间去噪 + 上采样 仍然不够 ——信息量不足
- 需要依赖 时间数据(Temporal Data) 来增加等效采样数
方案:有效片元计数器
- 灵感来源:Mattausch(2010) 论文 "High Quality Screen-Space Ambient Occlusion using Temporal Coherence"
- 关键机制:维护一个 有效片元计数器(Valid Fragment Counter)
- 计数器在以下情况下 重置 :
- 片元被 遮挡/去遮挡(Disoccluded)
- 片元邻域发生 变化 (如有物体在移动)
计数器越高 = 累积帧数越多 = 时间混合权重越大 → 结果越稳定平滑。
最终存储格式
| 格式 | 用途 |
|---|---|
| RGB9E5 | 标准质量(共享指数浮点,更紧凑) |
| RGBA 16F | 高质量模式 |
- 最终解码球谐函数结果后,存储为 最终辐照度(Final Irradiance)
逐步可视化对比
演讲者展示了每个步骤的可视化效果(中间步骤在球谐空间,因此展示的是解码后的辐照度):
| 步骤 | 效果 |
|---|---|
| 原始辐照度(无去噪) | 非常嘈杂,质量很差 |
| 空间去噪后 | 有所改善,但仍不够好 |
| 加入时间累积后 | 明显更平滑,质量"相当可以" |
| 加上方向性光照分量 | 最终结果 ,具备法线响应和光照方向性 |
高低画质对比
| 设置 | 特点 |
|---|---|
| 高画质(High Spec) | 完整的方向性信息和高频细节 |
| 低画质(Low Spec / 主机) | 方向性信息略有减少,缺失部分高频细节,但 整体质量在可接受范围内 |
演讲者指出两者差异在演示屏幕上可能不太明显,但低画质主要是 丢失了一些高频方向性细节 。这就是主机上使用的质量等级。
格式问题、间接高光反射与透明物体光照
纹理格式问题:R11G11B10F 的陷阱
实际遭遇的问题
- 项目中使用 R11G11B10F 格式存储辐照度数据
- 美术团队反复投诉:"是不是开了色彩分级(Color Grading)?为什么画面里看不到纯白色?"
- 根本原因:该格式的量化误差在时间累积技术中会快速放大
具体表现
| 问题 | 说明 |
|---|---|
| 色彩偏移 | 累积后出现 绿黄色调(Green-Yellow Tint) |
| 额外色带(Banding) | 精度不足导致渐变区域出现明显阶梯 |
| 无法表示纯白 | 格式本身的局限性 |
期望的解决方案:RGB9E5
- 希望未来所有硬件能 完整支持 RGB9E5 格式 :
- 写入渲染目标(Render Target)
- 混合(Blending)到渲染目标
- 从 Compute Shader 写入
- 原因 :显存增长速度 跟不上需求 ,团队需要大量共享/复用图像以节省内存
- 当然可以用 Dither 等手段掩盖色带,但演讲者认为 应该从根本上解决格式问题
间接高光反射(Indirect Specular)
反射探针(Reflection Probes)——近十年的基础设施
- id Software 使用反射探针已有 近十年 历史
- 废弃光照贴图的额外好处 :美术不再需要等烘焙完成,可以 随时更新反射探针
技术细节
| 参数 | 值 |
|---|---|
| 格式 | Cube Map Array + Block Compression |
| 放置方式 | 手动放置在关卡各处 |
运行时应用方式
| 表面类型 | 查找方式 |
|---|---|
| 不透明表面 | Tile Binning |
| 透明表面 | Clustered Binning 或 级联光照网格 |
新技术:反射探针结果重拟合(Refitting)
- 参考文献:Lazarov 最初提出,Hopson 近期改进
- 解决的问题 :反射探针在生成位置之外的较远距离使用时,会产生 漏光(Light Leaking)
核心思路
- 从反射探针中 移除原始辐照度(Radiance)
- 替换为本地的辐照度结果 ——即当前帧刚刚计算的帧辐照度数据
- 使反射结果与局部光照更加 一致和连贯
效果对比
| 重拟合前(左) | 重拟合后(右) |
|---|---|
| 某些物体看起来在"发光" | 更多红色光照反弹 |
| 光照不太协调 | 更多间接遮蔽,整体更 连贯(Cohesive) |
反射的完整回退链
基于平滑度的分层策略
表面平滑度(Smoothness)
│
├─ 低于阈值 ──→ 反射探针(最快,兜底方案)
│
└─ 高于阈值 ──→ 屏幕空间反射(SSR)
│
├─ 成功 → 使用 SSR 结果
│
└─ 失败(经常失败)→ 光线追踪反射(RT Reflections)
光线追踪反射的细节
| 参数 | 值 |
|---|---|
| 自带时间上采样器 | 是 |
| 分辨率(取决于质量) | 1/4 分辨率 或 半分辨率(两轴均缩) |
| 主机端性能 | 约 2 ms |
遗憾:主机上被禁用
- 尽管光线追踪反射运行速度不错(主机约 2ms),最终在主机上被禁用
- 原因 :不仅仅是反射本身的优化问题,而是需要对 整个帧的各个环节 进行进一步优化,才能在严格的帧预算内塞入所有功能
- 演讲者感叹:"完成后再发布——这种事如今已经不存在了"(暗示发售时间压力)
- 同样因时间不足而未能加入的还有 更复杂的 BRDF 支持 ,留待下一代引擎迭代
透明物体的光照:体积辐照度
设计思路
- 透明物体依赖 辐照度体积(Radiance Volumes) ——一种体积数据结构
- 允许在 世界中任意位置 索引查询该点的辐照度
- 这正是级联辐照度体积最自然的应用场景
第一个受益者:体积雾(Volumetric Fog)
- 基于 Bart Wronski(2014) 的演讲方案
- 体积雾系统直接从辐照度体积采样,获得全局光照效果
阶段性总结:整体架构的优雅之处
整个 GI 系统的设计体现了几个核心原则:
- 逐级回退(Fallback Chain) :从高精度到低精度,确保永远有结果
- 计算分摊(Amortization) :昂贵的操作分散到多帧
- 数据复用(Reuse) :同一套辐照度数据服务于多个渲染系统(不透明 GI、反射重拟合、透明物体、体积雾)
- 简洁性优先 :着色器种类少、数据结构简单、格式紧凑
透明物体光照复用、方向遮蔽与性能数据
体积雾辐照度复用于透明物体
发现与动机
- 最初为 体积雾(Volumetric Fog) 查询辐照度体积
- 开始处理其他透明物体时意识到:为每个透明表面重复计算辐照度是浪费的
- 既然体积雾已经从 近平面扩展到远平面 ,覆盖了完整的深度范围,为什么不直接复用?
实现
- 直接复用体积雾阶段的辐照度计算结果
- 同样使用 球谐函数(Spherical Harmonics) 编码
- 应用于 粒子(Particles)、玻璃(Glass) 等各类透明物体
反射探针重拟合也应用于透明表面
| 重拟合前(左) | 重拟合后(右) |
|---|---|
| 表面看起来 完全失真(Bogus) | 与环境 更加一致(Coherent) |
| 探针可能在发光表面附近生成,导致错误反射 | 移除原始辐照度,替换为本地辐照度体积数据 |
| — | 透明表面上还能看到更好的 间接阴影(Indirect Shadowing) |
延迟合成通道(Deferred Composite Pass)
流程位置
- 在所有 不透明渲染、全局光照更新、所有延迟步骤 完成之后
- 加载所有图像 → 合并 → 输出 最终不透明结果
方向遮蔽(Directional Occlusion)
解决的核心问题
补回缺失的高频细节 ——BVH/SDF 等加速结构中 不包含最高精度的几何 LOD:
- 小型植物
- 微小草叶
- 其他细节几何
演讲者坦言:"我们对 LOD 的使用非常激进",因此需要方向遮蔽来 重建丢失的高频信息。
视差遮蔽映射(Parallax Occlusion Mapping)的特殊需求
- POM 在项目中 几乎无处不在
- 表面可由 多达 8 层材质 组成
- 传统做法:为每个光源 逐层光线步进(Ray March)高度图 来计算阴影 → 极其昂贵
- 方向遮蔽的方案:解耦计算频率
额外收益
| 用途 | 说明 |
|---|---|
| 每个光源的接触阴影 | 无需为每个光源生成阴影贴图 |
| 间接阴影 | 在 POM 表面上获得微阴影(Micro-Shadows)和间接遮蔽 |
| 高光遮蔽 | 用于反射探针的 Specular Occlusion |
实现方式
- 极其简单——核心代码仅约三行
- 沿 逐像素法线定义的半球 进行光线步进
- 判断是否存在遮蔽 → 存储 平均未遮蔽方向(Average Unoccluded Direction)
- 原始结果 相当嘈杂 → 需要 去噪 + 时间滤波 ,与 GI 去噪流程类似
演讲者评价:"性价比极高(Bang for the buck)"。
性能数据:主机端真实场景
测试条件
- 热点场景 / 最坏情况 :大型远景 + 大量敌人 + 植被 + 粒子 + 运动物体
- 不是演示 Demo ,而是 真实游戏场景
各平台数据对比
| 平台 | 特点 |
|---|---|
| Xbox Series X | 基准性能 |
| PlayStation 5 | 与 XSX 大致相同 |
| Xbox Series S | 略慢(符合预期) |
| PlayStation 5 Pro | 因运行 更高分辨率 ,上采样成为瓶颈 ;但其他环节更快,整体 仍然很快 ,最终基本持平 |
关键信息
- 各主机平台性能 非常接近 ,差异主要来自分辨率目标和硬件规格的细微不同
- 系统在真实高负载场景下仍能保持目标帧率,验证了整体架构设计的可行性
这一段涵盖了透明物体的光照优化、方向遮蔽的多重妙用,以及最终的主机性能验证。如果还有后续内容(如具体的毫秒级分解、总结与未来展望),请继续分享。
异步计算收益、总结与问答
异步计算(Asynchronous Compute)的实际收益
| 平台 | 异步计算节省的时间 | 备注 |
|---|---|---|
| PlayStation 5 | ~0.5 ms | 显著收益 |
| Xbox Series S | ~0.5 ms | 显著收益 |
| Xbox Series X | ~0.4 ms | 稍少一些 |
| PlayStation 5 Pro | ~0.1 ms | 收益很小——因为世界采样、辐照度缓存、辐照度体积等 本身已经运行极快,异步计算能隐藏的开销有限 |
总结:整体成果
数量级的改进
| 维度 | 之前(光照贴图烘焙) | 之后(全动态方案) |
|---|---|---|
| 预计算时间 | 数小时 | 毫秒级(实时) |
| 磁盘存储 | GB 到 TB 级 | 基本为零 |
| 美术迭代反馈 | 等待数分钟到数小时 | 即时可见 |
| 创作自由度 | 受烘焙流程限制,可能需要削减关卡 | 无需妥协,美术可以尽情发挥创意 |
演讲者特别赞赏美术团队的出色工作:"如果你玩了这款游戏,你会发现美术方向(Art Direction)在我看来真的非常惊艳。"
技术一致性
- 静态与动态物体使用完全相同的代码路径 ——结果更加一致
- 透明物体 始终是"问题儿童" ,但也比以前更加一致
- 代码路径大幅简化
代码简化的深远影响
演讲者对此有切身体会:
"如果你写过自己的光照贴图烘焙器,你就知道——那是一个自成体系的小世界:光栅化器、光线追踪器、所有需要更新的数据结构,再加上云端处理所需的网络代码……"
- 现在,紧凑的技术团队 中的任何人都能进入代码、做出修改、立即在屏幕上看到结果
- 初级工程师也能上手修改 ,不再需要深入理解复杂的烘焙管线
- 这不仅是美术的迭代速度提升,也是 程序员的迭代速度提升
致谢
| 致谢对象 | 内容 |
|---|---|
| id Software 技术团队 | 核心实现 |
| 美术团队 | "不断推动极限,直到项目最后一刻还在要求新功能" |
| Natalia | 邀请演讲 |
| 粉丝群体 | "我们热爱你们所做的一切,没有粉丝的支持我们做不到这些" |
现场问答
Q1:本地辐照度体积(Local Irradiance Volumes)是什么?是美术手动放置的吗?如何与级联辐照度体积结合?
回答:
- 是的,这些是由 美术团队手动放置 的体积
- 根据设置,它们可以:
- 覆盖(Override) 级联辐照度体积的结果
- 或者 叠加(Add) 到级联辐照度体积的贡献之上
- 幻灯片的注释中有更详细的信息
Q2:扩散 GI 方案对动态事件的响应性如何?例如能否捕捉到移动 NPC 脚部附近的间接阴影?是否有手段隐藏延迟?
回答:
- 计算 分摊在多帧 上,因此存在一定的 时间滞后(Temporal Lag)
- 角色 存在于 BVH 中 ,且包含动画数据,因此 能捕捉到动态间接阴影
- 滞后程度取决于 时间累积参数的精细调优 :
- 在双边时间上采样核中使用 速度分量(Velocity Components) 来调节响应性
- 总结:"不完美,但高效运行(Not perfect but it runs efficiently)"
全篇总回顾
这场演讲完整呈现了 id Software 在 《DOOM: The Dark Ages》 中从光照贴图烘焙到 全动态全局光照 的技术转型:
| 核心模块 | 关键技术 |
|---|---|
| 加速结构 | 世界空间 BVH(Top-Level 每帧重建,Bottom-Level 增量更新) |
| 空间数据结构 | 级联光照网格(替代 Clustered Binning 远距离退化问题) |
| 世界采样 | 级联辐照度体积 + 可见性射线追踪(仅存储最小化打包命中数据) |
| 辐照度缓存 | 空间哈希(一维数组 + 量化位置/法线/LOD) |
| 着色 | 延迟着色,与光栅化共用代码;迭代式多反弹(读取上一帧数据) |
| 去噪 | 可分离双边高斯 → 双边上采样 → 时间累积(有效片元计数器) |
| 间接高光 | 反射探针(重拟合)→ SSR → RT 反射(三级回退) |
| 方向遮蔽 | 半球光线步进,用于高频细节恢复、POM 阴影、高光遮蔽 |
| 透明物体 | 复用体积雾辐照度 + 探针重拟合 |
最终成果:在所有主机平台上实现了实时全动态全局光照,同时为美术和程序团队带来了质的飞跃般的工作流改善。