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 GB约 5 GB
全游戏磁盘占用约 44 GB约 110 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 重叠测试
  • 将通过测试的结果写入 最终缓冲区 ,完成整个网格构建

性能数据

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

着色流程

  1. 使用 Ray ID 和 Probe 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 Index 和 Ray 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 阴影、高光遮蔽
透明物体复用体积雾辐照度 + 探针重拟合

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