idTech8 全局光照方案:Fast as Hell

Fast as Hell: idTech8 Global Illumination

演讲者与项目背景

  • 演讲者: 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 的处理流程

  1. 光栅化(Rasterize) :对每个 Tile 中的 Texel,光栅化与该 Tile 重叠的三角形
  2. 计算属性 :确定每个 Texel 的 世界空间位置(World Space Position)法线(Normal)反照率(Albedo) 等属性
  3. 路径追踪(Path Tracing) :启动路径追踪过程,计算全局光照
  4. 更新辅助结构 :更新所需的数据结构,包括 辐照度缓存(Radiance Cache) ,用于在所有 Texel 数据间 共享计算开销
  5. 编码存储 :最终将 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 GB5 GB
全游戏磁盘占用约 44 GB110 GB
全地图烘焙周转时间1 个月2 个月

增加机器是否能解决问题?

  • 当时已使用约 64 台机器 进行分布式烘焙
  • 即使 翻倍机器数量
    • 迭代速度 不会有质的提升
    • 内存和磁盘需求完全不变
  • 结论: 完全不可扩展(Not Scalable)

光线追踪的早期探索与经验教训

首次光线追踪实践

  • 2021 年 6 月 ,id Software 为 idTech7 发布了首个 光线追踪更新
  • 这次经验提供了非常宝贵的认知

核心教训

  1. 硬件并没有那么快 :能追踪的光线数量有限
  2. 低端硬件与高端硬件差距巨大 :可达 数量级 的性能差异
    • 低端基准线: Xbox Series S 以及 PC 端对应级别的 GPU
  3. 硬件性能增长有限 :从 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) 处理包含以下步骤(后续逐一展开):

  1. 构建必要的 数据结构 ,用于将计算 延迟到后续步骤
  2. 首先构建的是 级联光照网格(Cascaded Light Grids) ——这一结构在 Doom Eternal 的光线追踪更新中已经引入
  3. 该结构的核心作用:提供一种机制,能够在 世界空间中任意位置 进行光照查询

级联光照网格是整个实时 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 类型
    1. 光源(Lights)
    2. 反射探针(Reflection Probes)
    3. 贴花(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 重叠测试
  • 将通过测试的结果写入 最终缓冲区 ,完成整个网格构建

性能数据

平台耗时
PC0.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 的一贯原则:保持简单
  • 着色器种类 非常少 ——这极大简化了工程复杂度,让开发"更轻松愉快"

着色流程

  1. 使用 Ray IDProbe ID 推导出 全局射线索引(Global Ray Index)
  2. 加载对应的 射线打包命中数据(Ray Hit Packed Data)
  3. Shader Binding Table Index 确定需要访问的 三角形数据
  4. 利用 重心坐标(Barycentrics) 计算 命中点的世界空间位置
  5. 执行 标准的光照/着色循环 ——与 光栅化路径使用完全相同的代码
    • 通过 预处理宏(Defines) 排除部分代码段以优化性能

多次反弹的模拟:迭代式方法

  • 不追踪更多射线 来模拟多次反弹
  • 采用 迭代式过程(Iterative Process)
    • 每帧读取 上一帧的辐照度体积(Previous Frame Irradiance Volumes) 数据
    • 渲染的帧数越多 → 等效的 光照反弹次数越多
    • 这是一种 时间上的递归 ,避免了单帧内追踪多级光线的高昂开销

更新规模与复用策略

参数
每帧更新的缓存条目数20,000 个
缓存复用更新后的条目在 多帧内复用 ,帧数取决于质量设置

核心思想:避免每帧重复计算 ,已计算的结果尽可能跨帧复用。


辐照度体积更新(Radiance Volumes Update)

更新流程

  • 辐照度缓存的第一个用途:更新辐照度体积中的辐照度数据
  • 实现为 单次 Compute Dispatch
  • 每帧 只更新当前正在处理的体积 :即当前级联(Cascade)和当前本地辐照度体积(Local Irradiance Volume)

Thread Group 设计

参数说明
Thread Group Size8 × 8 = 64恰好对应每个采样点最多 64 条射线
  • Probe IndexRay Index 加载所需数据:
    • 探针位置(Probe Position) → 即采样位置
    • 射线命中距离(Ray Hit Distance)
    • 射线方向(Ray Direction)

缓存查找与 Group Shared Memory 优化

  1. 根据射线命中点计算 空间哈希值
  2. 索引到 世界辐照度缓存 获取着色结果
  3. 将结果存入 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,余弦加权 + 蓝噪声

核心设计哲学贯穿始终

  1. 延迟计算 :先记录命中,后续统一着色
  2. 分帧摊还 :昂贵计算分散到多帧
  3. 迭代式多弹 :通过读取上一帧数据模拟多次反弹,无需额外射线
  4. 缓存复用 :计算一次,跨帧和跨 Texel 反复使用
  5. 极致简洁 :少量着色器、简单数据结构、统一代码路径

最终收集(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 三个颜色通道的球谐系数)
通道内容
RL0 分量(环境光/均值)
G, B, AL1 分量(方向性信息,三个方向轴)
  • 之所以需要 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)

核心思路

  1. 从反射探针中 移除原始辐照度(Radiance)
  2. 替换为本地的辐照度结果 ——即当前帧刚刚计算的帧辐照度数据
  3. 使反射结果与局部光照更加 一致和连贯

效果对比

重拟合前(左)重拟合后(右)
某些物体看起来在"发光"更多红色光照反弹
光照不太协调更多间接遮蔽,整体更 连贯(Cohesive)

反射的完整回退链

基于平滑度的分层策略

表面平滑度(Smoothness)
    │
    ├─ 低于阈值 ──→ 反射探针(最快,兜底方案)
    │
    └─ 高于阈值 ──→ 屏幕空间反射(SSR)
                        │
                        ├─ 成功 → 使用 SSR 结果
                        │
                        └─ 失败(经常失败)→ 光线追踪反射(RT Reflections)

光线追踪反射的细节

参数
自带时间上采样器
分辨率(取决于质量)1/4 分辨率半分辨率(两轴均缩)
主机端性能2 ms

遗憾:主机上被禁用

  • 尽管光线追踪反射运行速度不错(主机约 2ms),最终在主机上被禁用
  • 原因 :不仅仅是反射本身的优化问题,而是需要对 整个帧的各个环节 进行进一步优化,才能在严格的帧预算内塞入所有功能
  • 演讲者感叹:"完成后再发布——这种事如今已经不存在了"(暗示发售时间压力)
  • 同样因时间不足而未能加入的还有 更复杂的 BRDF 支持 ,留待下一代引擎迭代

透明物体的光照:体积辐照度

设计思路

  • 透明物体依赖 辐照度体积(Radiance Volumes) ——一种体积数据结构
  • 允许在 世界中任意位置 索引查询该点的辐照度
  • 这正是级联辐照度体积最自然的应用场景

第一个受益者:体积雾(Volumetric Fog)

  • 基于 Bart Wronski(2014) 的演讲方案
  • 体积雾系统直接从辐照度体积采样,获得全局光照效果

阶段性总结:整体架构的优雅之处

整个 GI 系统的设计体现了几个核心原则:

  1. 逐级回退(Fallback Chain) :从高精度到低精度,确保永远有结果
  2. 计算分摊(Amortization) :昂贵的操作分散到多帧
  3. 数据复用(Reuse) :同一套辐照度数据服务于多个渲染系统(不透明 GI、反射重拟合、透明物体、体积雾)
  4. 简洁性优先 :着色器种类少、数据结构简单、格式紧凑

透明物体光照复用、方向遮蔽与性能数据


体积雾辐照度复用于透明物体

发现与动机

  • 最初为 体积雾(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 阴影、高光遮蔽
透明物体复用体积雾辐照度 + 探针重拟合

最终成果:在所有主机平台上实现了实时全动态全局光照,同时为美术和程序团队带来了质的飞跃般的工作流改善。