"In recent years, volumetric rendering techniques have become more popular for real-time rendering in video games. However, they are usually limited to specific use cases like clouds or volumetric fog, or they require a lot of custom setup work to achieve a believable result. With *007 First Light*, we introduced *Smolder*, a fully integrated, scalable, real-time volumetric effect rendering framework, into our Glacier engine. Smolder enables us to use volumetric effects as if they were any other type of VFX. It is fully integrated with all lighting systems in Glacier, as well as with the various layers in the scene — opaque, transparent, and volumetric fog. This enables our artists to work freely with Smolder when building the environments, cinematics, and in-game moments, and it enables us to add player-controlled volumetric effects to Bond's various gadgets. This presentation provides an in-depth look at how Smolder works, as well as the path we took to get there. It evaluates the benefits and drawbacks of the various parts of the Smolder algorithm and concludes with a look at where Smolder can go from here."
- 要解决的是「通用性」而不是「某个效果」:Smolder 的验收标准是美术能在引擎里像放普通 VFX 一样摆放体积效果,而不是程序为每个效果写一套。
- 四件事必须一起解:散射积分与光照、三类介质(不透明 / 透明 / 体积雾)的叠加与排序、重叠体积、以及性能。讲座的顺序基本就是这个顺序。
- 两次失败是主线的一部分:第一次是把粒子软件光栅化进体素网格(p26–p34),第二次是想把渲染拆成三趟独立处理(p75–p78),两次都被生产现实打回。
- 转折点是一句反直觉的话:低分辨率加大步长,也能拿到要的视觉质量(p36–p38)。这句话之后系统才真正开始跑起来。
- 一份可直接引用的自陈缺口:p110 给出 next todos 清单;p106–p108 的性能读数分三档硬件,作者未给统一测试场景。
1把体积效果当普通系统用
把体积效果当普通系统用
这件事的推手来自美术侧。IO Interactive 的首席技术美术 Martin Nielsen 带着几个动态玩法 VFX 找到讲者,那些效果需要一个「到处都能用」的体积渲染器(p5)。Martin 更关心结果,讲者的关心点落在工作流上:他要让美术在 Glacier 里像用别的任何系统一样自由地做体积效果,要把体积效果变成引擎里的一等公民(first class citizen)(p5)。p5 的幻灯片把这段诉求拆成几次点击逐条展开,最后落在一句原话上:想要一个「just works」的系统,美术把体积放进世界时不必去想任何限制,可以只专注在自己的艺术构想上。
这段诉求还有一个具体的来处。Martin 带来的那几个动态玩法 VFX 属于另一个项目,并不在 007 First Light 里(p5)。他要的是一个到处都能用的体积渲染器,而讲者从这件事里读出的是工作流层面的机会:Glacier 按系统化玩法与快速迭代来设计,一个功能只要能被美术随手调用,它就会顺着引擎的迭代节奏被反复使用(p4、p5)。
讲者在这段里表露的优先级排得很前。任何一套渲染系统里,对他最重要的是好工作流能带来什么;技术再惊人,美术用不下去、不爱用,做出的结果就不会一样,那这套技术也就没有意义(p5)。这句判断解释了后面所有取舍的方向,第 8 章把「几乎无法创作效果」列成放弃第一条路线的首要原因,p32 的原话就接在这里。
先把这套系统在项目里的位置摆清。007 First Light 在讲者开讲前约两个月发售,发售版本带进了大量体积效果,预告片里能看到其中一部分(p2)。这些效果由一套自研渲染系统驱动,名字叫 Smolder;美术把它用在布景、玩法小道具、过场动画与激烈动作段这几类场合(p3)。游戏跑在自研引擎 Glacier 上,这套引擎支撑工作室的全部项目,设计上围绕系统化玩法与快速迭代(p4)。讲者对自研引擎的评价留了余地:它并不总像期望的那样顺畅,顺畅的时候人与工具能进入一种状态,做出很好的东西(p4)。产品侧的证据摆在同一组页面上:p2 那支发售预告片里能看到这些效果的实际产物,Smolder 覆盖的场合从布景一直排到激烈动作段(p2、p3)。四类场合叠加起来,一套专用系统要同时满足的画面变化幅度就超出了它的设计范围。
第二条诉求在 p6 上,那一页的页内文字被截断,能读到的部分是:他们也不希望美术去操心目标硬件。讲者见过相当多游戏,渲染技术在 PS5 或 XBOX Series X 这类高端平台上效果很好,美术随后却要为降级系统再做一套内容。这条与第 5 章那份平台清单是同一条约束的两端。
动手之前,团队先盘了一遍市面上已经运出去的东西(p7)。清单不短:froxel 体积雾(Glacier 当时已有,画面效果不够好)、体积云(Andrew Schneider 在 Horizon 系列上的工作带火了这一类)、Counter-Strike 2 的烟雾弹,等等。讲者给这些系统归了三条共通的限制:它们通常高度专用(purpose built);不少系统的场景集成有限,透明这一块尤其明显;不少系统的光照集成也很有限,通常只支持几个光源、几乎没有投影光源(p7)。三条限制对上这一章开头那两条诉求,缺口在哪就清楚了。p7 的收口句是全文的目标定义:他们要的是一个通用(generic)、完全集成(fully integrated)、性能可用(performant)的体积效果渲染器。
盘点的范围不止于游戏成品。讲者把往届 Advances 里以体积为主题的演讲一并算进这份参考清单(p7),这说明他们手里的候选方案在公开资料里并不稀缺,稀缺的是能同时满足三条限制的那一个。p3 列出的四类使用场合也各有不同的代价重心:布景对画面质量敏感,小道具与激烈动作段对性能与快速移动敏感,这份差异会在第 5 章的平台清单里再被提起。
这里先把后面的代价记一笔。三条限制里最难兑付的是「完全集成」,它要求体积效果与场景里已经存在的介质共存,而这一块恰好是多数现成系统的短板(p7)。第 4 章会把这份短板拆成不透明、透明与体积雾三层的先后关系,第 6 章再处理透明那一层里排不掉的顺序。
这一章能核对的结论有三条。需求侧来自一位技术美术的具体 VFX 需求,加上讲者对工作流的判断,两者在 p5 合成一句「一等公民」。对手侧在 p7 归纳成三条限制:专用、场景集成有限(透明尤甚)、光照集成有限(光源少、投影光源少)。目标侧在 p7 被写成三个词:通用、完全集成、性能可用。这三个词怎么拆成可解的工程问题,要在下一章的总览里看。
2散射这一半:一遍步进要算清的两笔账
承上一章那三个词往下走,「通用」与「完全集成」都要求先把最基础的积分讲透。讲者按 p8 做了一段铺垫,对象是没做过体积渲染的听众。
体积渲染要算两件事:体积背后的光有多少透了过来,以及有多少入射光被散射向相机(p8)。做法是光线步进(raymarching)。主光线上每走一步,就采样一次体积的密度;密度用来算光深(optical depth),后者是透明程度的度量;沿着主光线逐段拼出透射率函数(transmittance function)(p8)。这段铺垫里带了一个前提假设:每一步内部的密度被当作均匀,密度一不均匀就会引入误差,此时需要更小的步长,否则会欠采样(p8)。讲者在 p8 顺手给了深入材料,指向 Scratchapixel 那套关于光线步进的系列,评语是做得很好。
步长与误差的关系要摆清。步内密度均匀这个假设一旦不成立,误差就随步长放大,代价是要把步长减小、采样数上升(p8)。第 5 章那条「按体素大小调步长、在重叠区取更小步长」的做法,处理的就是这件事:先用存储格式把密度变化描述清楚,再决定每一步该跨多远。
这两件事在页面上被摆在一起讲,容易看漏它们各有一套量。透过来的那部分由光深决定,光深是密度沿光线的累积;散射进来的那部分要乘上一个相函数(phase function),讲者在后面讲 GI 时才点明这个前提,体积侧做整球积分正因为没有采用各向同性相函数(p8、p13)。这两处口径要在第 3 章合起来看。
散射那一半在 p9 上:要为每个采样点确定入射光的量。光要穿过体积才能到达采样点,于是入射光这一侧也要算一遍透射率(p9)。p10 把完整算法串了一遍,顺序是走一步、采样密度、把透射率更新到当前位置,然后对光线做一次次级光线步进(secondary raymarch)算出光线的透射率,据此得到该采样点的散射;这份散射先被主光线的透射率衰减,再累加进结果;主光线继续走到下一个采样点,直到抵达体积末端,或者透射率降到零就提前停止(p10)。提前停止这一步有明确理由,讲者说后续采样不会再对结果有贡献。
提前停止省下来的那份开销,讲者算得比较直接:走到体积末端或者透射率归零就停,后面的采样不再贡献结果(p10)。这条规则在真实场景里的收益取决于烟雾有多厚,第 11 章按采样率跟踪光照采样时用的仍是同一个判据,只是把判据的粒度从单条光线放大到整个波。散射那一步的顺序也要记清,先取该采样点的散射,再乘主光线的透射率做衰减,最后才累加进结果(p10),这两笔账的先后决定了后面「透射率函数可以提前算出来」这种做法为什么成立。
这套结构还有一个后果要记住:主光线每走一步都要停一次,入射光那份透射率就在这个间隙里被算出来(p10)。讲者在 p8 到 p10 只给机制,页面上没有任何性能数字;第 11 章把采样数列成首要成本项,源头就在这里。第 5 章的平台清单还会反过来给这条循环设一个上限,能跨多大步、能走多少步,最终由最弱的那个平台决定。
这套铺垫定下了后面所有困难的形状。让每一步都算一次入射光,意味着光照有多复杂,积分就有多贵;把透射率写成沿主光线逐段拼出来的函数,意味着场景里任何一层介质要插进来,都得嫁接到这条函数上。p8 的两件事、p9 的次级步进、p10 的三步循环与提前停止,是后面光照、透明与性能三条线的共同起点。
这一章可以核对的结论是三条机制,页码都落在纸上。要算的量有两个:透过体积的光与散射向相机的光(p8)。透射率是逐段拼出来的函数,拼的前提是「步内密度均匀」,不满足就要缩小步长(p8)。入射光那一侧要走第二次步进,散射结果先被主光线透射率衰减再累加,透射率降到零就提前停止(p9、p10)。三个词里「通用」与「完全集成」会在第 3 到第 6 章被拆成具体约束,「性能可用」在第 5 章给出硬件清单。
3让 150 到 200 盏局部光都进得来
上一章把每个采样点的账算到了入射光那一侧,这一章就把光源侧的复杂度摊开。讲者从太阳开始(p11)。007 First Light 里的太阳直射光带三种阴影:传统的级联阴影贴图(cascaded shadow maps),用 3 级级联,覆盖 25 米;再往外是一层跟随相机滚动的级联,只装静态阴影,覆盖 200 米;剩下的地图范围交给一张分辨率很低的全局阴影贴图。007 里最大的地图是 4 乘 4 公里,这三个数一摆,阴影的尺度分档就清楚了(p11)。三种阴影对应的资源不同:25 米那一层由三级级联负责,200 米那层是跟随相机滚动的静态阴影级联,再往外靠一张低分辨率贴图兜底(p11)。分层的意思是同一束太阳光在不同距离上由不同资源负责,体积里的阴影采样要在这三层之间切换。
局部光的问题出在数量上。按 p12 的说法,他们的光照美术喜欢到处放灯,最重的视图里有 150 到 200 盏局部光。当时还在用一套很老旧的、基于 tile 的光剔除系统,这套东西最初是为《杀手 2016》做的;p12 那张 debug 视图里,单个 tile 最多叠到 60 盏灯。数量之外还有种类:要支持全部光照类型,每种类型又带一堆特性,讲者把这份代价记在寄存器压力(register pressure)上(p12)。
第三块是全局光照(p13)。007 这一代加了一套完整的实时 DDGI 风格 GI。探针的存法按常规:irradiance、radiance 与 depth 存在 8×8 的八面体(octahedral)贴图里;3 级级联,每级 64×64×16 个探针,最小网格层的探针间距 60 厘米,讲者的评价是这套网格相当密。采样代价高,取一次要采 8 个探针再做三线性插值。对普通表面,每个探针取一个 irradiance 采样就能得到给定法线半球下的预积分结果;体积上这条捷径不成立,体积需要对入射辐亮度在整个球面上做完整积分,若不采用各向同性相位函数(isotropic phase function),这份积分就得在运行时做(p13)。
把三块合起来看,光源侧的量级是这样:最重视图 150 到 200 盏局部光(p12),太阳光自己带三层阴影(p11),GI 那套网格在最小层上探针相距 60 厘米(p13)。每一条都要在体积的每个采样点上被求值,第 2 章那条步进循环把这三条乘在了一起。讲者把支持全部光照类型与特性的代价记在寄存器压力上(p12),这份压力直接对应同一时刻能驻留多少线程,会限制波内并行的宽度;第 11 章把波相干性当作 Smolder 的核心架构优势来讲,与这里记下的那笔账首尾相接。
这套 GI 数据的空间分辨率相当细,体积却要用整球积分的方式去取光,取一次就要碰到 8 个探针,两次采样成本叠在一起(p13)。讲者在这一页把结构参数给齐了,页面上没有附性能读数,代价量级要到第 11 章的优化里才见到。
阴影那一半单独占了三页(p14 到 p16)。p14 点出的第一个毛病是阴影光线的代价:它不只在规模上很差,对波相干性(wave coherency)与缓存命中率也不友好,积分过程中每条光线都要采体积的大片区域;紧挨着再放一条光线,第二个毛病就出来了,同一片体积被反复采样,讲者用的词是采样大量重复。p15 把代价再乘一层:要看到环境上的阴影,每个像素、每个表面都要朝光源再射一条阴影光线。p16 说清最后的量级:如果想让所有光源都投体积阴影,这份光线量根本不会发生;讲者也明确要所有阴影实时,烘焙阴影对工作流又是一层限制。
阴影光线的第二个毛病要单独摆开。相邻两条光线采的是同一片体积,重复的部分占了大头;相干性差与缓存命中率低又互相加重,同一个 wave 里的线程访问的体素并不相邻(p14)。p15 与 p16 把这份代价叠了两层,先按像素、按表面各加一条阴影光线,再按光源数量乘一遍,p16 那句「根本不会发生」针对的是这条乘法链的整体结论。讲者对烘焙阴影的排斥也带在工作流这条线上:烘焙意味着光源一动、体积一动就要重烤,那等于把第 1 章那条「随手放」的诉求收回去(p16)。
这一章的结论可以逐条核对。阴影的尺度分档是 3 级级联 25 米、滚动静态级联 200 米、低分辨率全局贴图,地图最大 4 乘 4 公里(p11)。局部光在最重的视图里 150 到 200 盏,旧 tile 系统下单 tile 最多 60 盏(p12)。GI 探针是 8×8 八面体贴图、3 级级联各 64×64×16、最小层间距 60 厘米,且体积侧必须在运行时做整球积分(p13)。阴影光线同时踩了相干性与重复采样两个坑,加上逐表面逐光源再加一条的乘法关系,p16 的「做不了」是这三页合起来的结论。光照这一侧定下的量级,会直接乘到第 2 章那条步进循环的代价上,也会在第 11 章的排序与剔除里再出现。
4三层介质叠在一个像素上
第 3 章的光照账落在一个采样点上,这一章把同一个像素上要叠的东西摊开,讲者从场景集成讲起(p17)。
不透明这一层最简单:步进走到该像素的深度就停。这里唯一的硬要求是逐像素精度,因为深度不连续处会出瑕疵(p17)。透明这一层难得多。透明面与体积不重叠时,问题容易解决;一旦重叠,体积要遮挡透明面,透明面也要遮挡体积,形成循环依赖(circular dependency),在这套基于光栅化的管线里,两者无法在同一个 pass 里渲染(p17)。p18 往这条依赖链里再放一个系统:把体积雾算进来,链上就有第三个成员。
不透明那一条的要求说起来简单,代价落在数据精度上。步进在像素深度处截断,截断位置只要差一点点,深度不连续的位置就会露出瑕疵,因此这条要求被写成逐像素精度(p17)。这条要求后面还会以另一种形式回来,第 11 章在降低光照采样率时仍要保住逐像素深度测试,理由就落在这里(p17)。
循环依赖可以说得更具体。体积要在透明面之前解出,才能挡住透明面;透明面又要在体积之前解出,才能挡住体积(p17)。两个方向互相要求对方先完成,在单趟光栅化里无法闭合。把体积雾放进来之后,链上的成员变成三个,每个成员都要在剩下两个之间找位置(p18)。p24 给出的解法是把这条链拆开:只要三套系统各自先产出一条透射率函数,函数之间就能按任意顺序拼接。
讲者在 p17 用的词是「circular dependency」,他在那一页没有给出解法,只是把这条依赖摆出来。p24 才给出方向,让三套系统各自先算出一条透射率函数,再把函数拼起来。中间隔了七页,这七页里的失败尝试与顿悟,构成了第 7 章与第 8 章的内容。
第三块是重叠体积(p19)。要让美术自由摆放体积,多块体积互相遮挡就得支持。最便宜的做法是把采样位置变换到物体空间,判断是否落在体积包围盒之外;问题有两个,逐体积逐采样地做这些矩阵乘法累积得很快,而结果也不精确,体积很少是规则的盒子,会带出大量假阳性(p19)。讲者想要的效果更进一步:把体积周围的空区域整块跳过,对那些没有任何重叠的像素,干脆连步进都不要启动(p19)。
这一条还留了一个数量上的口子。讲者在 p19 只描述了最便宜做法的代价结构,没有给出体积数量的上限。这条上限要到第 11 章才出现,那里他把 007 里的重叠体积数限制在 4 块。三块要求之间的次序也有讲究,不透明那层只需要逐像素深度,排在最先;透明与体积雾要互相等待对方的透射率,排在后面(p17、p18)。
重叠体积那两种做法的代价分别落在两边。逐采样变换的代价是矩阵乘法乘以「体积数乘采样数」,讲者的原话是这种乘法累积得很快;不精确的代价是假阳性,因为体积很少是规则盒子,判定「在体积外」会放过本该跳过的采样(p19)。两种代价的方向彼此拉扯,想把假阳性压下去,就要拿更复杂的求交来换。
p19 给出的方向是把这份判定提前。理想形态的两句话是跳过体积周围的空区域、对不重叠的像素连步进都不启动(p19)。这一步如果做成,逐采样的判定就从渲染循环里消失,换上的是渲染前算好的一张查表;第 11 章的分段(segment)、位掩码(bitmask)与「8 个深度值、16 位够用」的存储,就是这张查表的具体形状。
这一章能核对的是三条约束。不透明侧的唯一要求是逐像素精度,避免深度不连续处的瑕疵(p17)。透明侧与体积互为遮挡,循环依赖使两者不能同 pass 解决(p17),体积雾接进来后链上有三个系统(p18)。重叠体积侧最便宜的做法不精确、代价随体积数累积,目标是整块跳过空区域与不重叠像素(p19)。这三条合起来要求的是一种「提前算好、按像素或按线程组查表」的结构,第 11 章的段与位掩码就落在这一章的问题上。
5质量与平台的账:不给低端机另写一套内容
场景集成这一半讲完,需求清单只剩最后一组,讲者把它放在视觉与性能两头(p20)。
视觉这一侧的要求是系统必须满足当代(current gen)保真度:效果若融不进环境、撑不起某些作品更写实的美术方向,对那些作品就基本不可用(p20)。性能这一侧的范围要从高端一路铺到低端,讲者报出来的目标硬件是一张 5090 在顶端,低端是 Switch 2、Steam Deck、Series S,PC 上还有 GTX 1660 与 RX 5700(p20)。这段里代价最高的一条被单独点了出来:他们最想避免的事情,是为低端平台再做一套降级系统、把内容做两遍(p20)。
这份清单的跨度要用一句话量一下:同一套算法要在顶端的 5090 与低端的掌机、老卡上同时成立,档位之间隔着好几代硬件(p20)。讲者担心的是同一份内容做两遍带来的长期负担,这条约束会一路影响到第 11 章,Series S 上那份减半分辨率的 mip 就是被它逼出来的。
p20 还点了一个被广泛采用的折中。不少现有的体积系统靠时域滤波(temporal filtering)在质量与性能之间取平衡;效果快速移动或者贴近相机时,因为缺运动矢量(motion vectors),这种折中会带来很重的残影(ghosting)(p20)。这句话在整场讲座里会反复回响,后面 TAA 与体积互相折腾的几页都在处理它留下的尾巴,第 12 章会把它列成一条边界。
视觉与性能这组要求还有一层互相牵制。当代保真度把算法往高质量一侧推,平台清单把同一套算法往低开销一侧拉(p20)。多数系统用时域滤波来消化这份张力,体积效果恰好缺运动矢量,快动与贴相机两个条件下重投影失效,残影随之出现(p20)。p20 这段话给出的判断是这条路不能直接照搬。
把这份清单与第 2 章那条步进循环对上,压力落在哪就清楚了。体积的代价随屏幕上的覆盖面积增长,同一套算法要在若干代硬件上成立,屏幕上的体积越大、越靠近相机,能承受的体积尺寸就越小(p20)。这条关系讲者要到讲优化时才明确说出来,起因却写在 p20 的清单里。
视觉这一侧还有一个隐含要求。p20 说效果必须融入环境,对体积来说这意味着光照与阴影都不能是假的,体积里的每一处明暗都要与场景里的光源、GI 与阴影保持一致(p20)。第 3 章那三组要求就是这份一致性在光照侧的展开。
进入实现之前,讲者补了两套要一起看的系统(p21)。体积雾用近乎标准的 froxel 网格做法;讲者强调的关键点是它的性质:这是一次体积式、从前到后(front to back)的积分,中间结果存在一个与视锥对齐的栅格(frustum aligned grid)里(p21)。
把栅格映射成透射率函数之后,系统之间的接口就统一了。体积雾、透明面与 Smolder 三套东西各自输出一条函数,再由一条统一函数把它们接起来(p22)。第 6 章处理这条函数在透明一侧的存法,第 8 章与第 11 章分别处理它在阴影与最终集成上的用法。这套接口的代价也要记着:三条函数一旦要互相施加,分辨率不一致的问题就得一并解决。
这一段还提前说出了后面对 TAA 的依赖。体积侧没有运动矢量可用,时域累积这条路被排掉之后,噪点就只能靠逐像素的蓝噪声去摊开(p20)。后面那套先抖动采样深度、再抖动插值权重的做法,动机都接在 p20 这句话上。
两头的账最后要合到一处:视觉质量决定这条函数要有多细,平台清单决定它能有多大。前者的读数在第 11 章那两组优化里被反复核对,后者在 p106 那几页的真机数字里出现,而那一组数字的硬件差异,第 12 章会单独说明。
讲者把这份清单讲完时给了一句自评,要解决的问题排成了一张不短的清单,前面等着的是一个不小的挑战(p20)。这句话既是需求部分的收口,也把第 3 章到第 6 章这几组约束合在了一起。
这一章的结论分两头。两头的交点是「一套内容、全平台」,p20 给出的目标硬件清单从 5090 一直排到 GTX 1660 与 RX 5700,中间还有 Switch 2、Steam Deck、Series S;避免降级与二次创作是这条约束的硬边界。折中那条路(时域滤波)在快动或贴相机时会在没有运动矢量的地方留下残影,这是后面几章反复要付的账。p21 与 p22 把 froxel 栅格与光线步进统一成同一种表达(透射率函数),这一步是三层介质能拼到一起的前提条件。
6透明排不掉序,就换成与顺序无关的透射率
上一章把透射率函数定成了三种系统共用的表达,这一章处理透明那一半,起点是排序这件事本身(p23)。
传统做法为了在单个 pass 里正确积分透明面,会把它们从后到前(back to front)渲染,每个面的不透明度由一个 alpha 值表示(p23)。这种情形下同样能画出一条透射率函数;麻烦出在排序的粒度上。排序按绘制(per draw)这个粒度来做,逐像素的粒度不在其中,讲者说这通常会带来很明显的闪烁瑕疵(p23)。绕开排序的一族方法叫顺序无关透明(order-independent transparency,OIT)。
p24 把这族方法的共同思路说清了。所有 OIT 算法都在用某种方式摆脱对 CPU 侧排序的依赖:或者在 GPU 侧做逐像素排序,或者把透射率积分与表面渲染解耦(p24)。解耦这条路的价值在于,如果透射率函数能提前拿到,透明面就能按任意顺序渲染,只要在那个深度上采样并施加对应的透射率即可(p24)。讲者把这类技术的综述指向去年 Michal Drobot 的演讲。
排序的粒度这一条可以再展开。按绘制排序意味着同一批透明几何共用一次排序结果,像素级别的前后关系被忽略了;讲者把这种忽略的可见后果说成很明显的闪烁瑕疵,效果一旦运动,错序就随帧变化(p23)。p24 那句摆脱对 CPU 侧排序的依赖,针对的就是这份按批次的粒度。
两条路各有代价。逐像素排序把排序工作放到 GPU 侧,需要在显存里多存一份像素级的数据结构;把积分与表面渲染解耦,则要求透射率函数先于表面渲染存在(p24)。他们选的是后一条路,它把函数与表面的时间顺序倒过来,先有函数、再有表面。讲者在 p24 指向 Drobot 的演讲,说明这一步在该领域已有多种做法可选。
他们自己用的是基于矩的顺序无关透明(moment-based OIT,MBOIT)(p25)。这套做法把透射率函数存成光深(optical depth)的一系列幂矩(power moments),另外单独存一个精确的 0 阶矩,后者装着光线末端处的光深(p25)。这里有一个关键的设计选择:矩存在对数空间(logarithmic space)里,于是矩的混合从乘法问题变成加法问题,在 GPU 上生成这条函数变得极其容易(p25)。矩还有一项好性质,它们完全可微,因此可以自由地做滤波(p25)。
代价写在 p25 的后半段,是一笔很实的存储账。讲者说矩极度吃存储:他们用 6 个矩,每个 32 位;试过 16 位矩,得来的瑕疵太明显,只好放弃;逐像素算下来总共要 224 位,这个量很重,给显存带宽与缓存带来了很大压力(p25)。
对数空间这一步的作用可以只从混合方式上理解。矩要在多个透明面之间被合并,在对数空间里这一步是加法,讲者的判断是在 GPU 上生成这条函数因此变得极其容易(p25)。同一份表示还有可微这个性质,滤波可以自由做(p25)。存储那笔账要把数拆开核对:6 个矩各 32 位,加上那个精确的 0 阶矩再占 32 位,一共 7 个 32 位量,逐像素合计 224 位(p25)。这 224 位要随每一帧的每个像素走一遍显存带宽与缓存,第 11 章的多项优化都在设法把这份压力降下来。
把这一章与第 5 章接起来看,两页处理的是同一条函数的两种性质。p22 关心栅格与步进如何产出这条函数,p25 关心这条函数在透明一侧用什么形式存下来(p22、p25)。形式的选择把两种代价换了位置:排序的自由度被换成了每像素 224 位的存储与带宽。
MBOIT 这套做法在该工作室有体积之外的来源。p25 讲的是他们在透明渲染里已经用了的矩表示,Smolder 后面直接借用了它(p25)。第 7 章那次尝试的起点就在这里,既然矩能生成透明面的透射率函数,把它与体积的矩相加,两套系统的集成就变得简单。
这一章可以核对的数有三个。矩按光深存,6 个矩每个 32 位,加上那个精确的 0 阶矩,逐像素合计 224 位(p25)。16 位矩试过,瑕疵太明显,被放弃(p25)。矩放在对数空间里,混合由乘法变成加法(p25)。存储口径还留了一个后续问题,6 个矩、每个 32 位是透明渲染的既有配置,体积侧能不能用更省的形式存同一条函数,讲者在这一页没有回答;后面讨论阴影时给出的口径是 4 个矩加 16 位量化。这三条合起来给出的是一种表示法:它把透明这一侧的排序问题绕开,代价落到存储与带宽上。到这一步,需求清单上光照、场景集成、质量与性能三块都已经摊清,透明这块也有了自己的量;后续要做的,是找出一条把这几块同时装进去的渲染路径。
7第一次尝试:把粒子软件光栅化到体素网格
上一章把透明那一半的账算清了:透射率(transmittance)可以用一组光学深度(optical depth)的矩(moments)来表示,代价是每像素 224 位(bit),对内存带宽与缓存都是压力。本章讲的是这套思路被直接搬到体积效果上的第一次尝试,材料是 p26 到 p31 六页讲者备注。
起点是一个看上去顺理成章的假设。p26 说得很直白,核心想法很简单,把同一套与顺序无关的透明(order-independent transparency,下文简称 OIT)算法套到体积上。先生成体积累积的透射率矩,再在两个系统各自解算(resolve)之前把它们加到透明的矩上,集成(integration)这一步就变得很简单。p26 里放了一段很早期的测试画面,那是这条路线唯一的现场记录,画面内容讲者没有逐项讲解,只标了它出自非常早的尝试阶段。
阴影那一头是同一件事的另一面。要支持自阴影(self shadowing)以及各类表面上的阴影,就需要一份能在任意深度采样的透射率表示。讲者给的理由是,有了这份表示就能去掉重复采样(duplication)的代价,阴影只以很高的 wave 相干性(wave coherency)渲染一次,之后按需采样。这句话在第 11 章会重新出现,因为它是这套系统最终能落地的那根线头。
密度建模走了另一条路。p27 交代得很清楚,他们决定用 GPU 粒子(GPU particles)。当时团队里一位程序员 Asger Meldgaard 正在做一套实时流体模拟(real-time fluid simulation)系统,拿它当底子可以免费得到很多能力,于是决定跟着走。紧接着是一句成本提醒,对大量粒子做 raymarch 并不便宜,尤其要在生成(generate)与解算(resolve)两趟里各做一次。早期也试过加速结构(acceleration structures),p27 的原话是结果不太令人鼓舞(not very encouraging)。
于是有了 p28 的方案,在 compute shader 里对粒子做软件光栅化(software rasterize)。每个线程渲染一个像素。先算出有哪些粒子与这个像素重叠,再对每个粒子解析地算出透射率并累加成矩;散射(scattering)在第二趟用同一套逻辑渲染,与 OIT 类似。粒子在这里被简化成带密度衰减的球体(spheres with a density falloff)。这套做法的好处写在 p26,阴影只需渲染一次,重复采样那笔开销被拿掉。
这套做法没能规模化。讲者给的原因是尺寸失衡,中等大小的粒子很快就会盖住几百、甚至上千个像素,而一个大效果里有几十万个粒子。两边的量级摆在一起,逐粒子逐像素的那点解析计算立刻变成主项。对策写在 p28 末,多尺度渲染(render this system at multiple scales),按粒子尺寸把它只画到某一档 mip 上,目的是让每个粒子覆盖的像素数大致相同。
mip 全部渲染完之后,p29 交代了展平(flatten)的办法,把各档 mip 的矩直接相加。这里有一处细节,讲者说线性过滤(linear filter)会留下很明显的伪影,所以改用三次过滤(cubic filter)。这一步等于给整套管线又加了一层需要调参的环节,而后面要看到的伪影就出在这层「先分档、再相加」的结构上。
结果先给一段承诺,再给两处问题。p30 的画面是 Glacier 引擎的素材,讲者自己说明这段不属于 007 First Light,他为了演示把视频裁掉了很多,并要求听众权当它是 007 的素材来看。承诺那一侧,实时流体模拟在起作用,GPU 粒子让团队在测试早期就拿到复杂效果,还免费得到与地面碰撞(collision with the ground)这类特性。
问题那一侧落在两段视频的差别上,差别只有一个变量,粒子尺度。左段锐度(sharpness)够,但能看见单个粒子的点(dots);讲者说那时的粒子数已经推得很高,可效果体量太大,仍然不够。右段把粒子放大,点没有了,效果却明显发糊(blurry),讲者用「超出美术方向(art direction)能接受的程度」来定性。同一个效果,粒子数往上走也解决不了,尺度往上走又牺牲清晰度,这一步没有第三条路可走。
p31 还有一句总结,讲者说这成了他们在这套系统开发过程中的一个模式,潜力很大,却总有一堆越来越难修的问题,小问题要动算法做大改,对性能也没有帮助。这句话是第 8 章复盘那一段的前奏,第 8 章会把它拆成四条具体判定。
本章可以核对的结论有两条。第一条是路线的形状,第一次尝试把密度交给 GPU 粒子、把透射率交给矩,用「生成 + 解算」两趟来跑(p26 到 p28)。第二条是它卡住的位置,规模化靠多尺度 mip 打补丁,代价落在两处能够指认的画面上,一处是放大粒子之后的糊(p30),一处是多尺度渲染在前景边缘留下的深度测试伪影(p31),后者讲者明确说了未解决。
8失败与顿悟:一台滑雪假期的启发
上一章的结论是第一次尝试卡在规模化与两处伪影上。本章 p32 到 p39 讲复盘与转折,这一段是从「放弃」走到「重新挑路」的全过程。
p32 是一份完整的失败清单。讲者说他们进入这条路线时就知道可能不成,现实是「一直在谈同样的问题却看不到解法」。随后是四条判定,太复杂;性能没有按预期扩展,尤其阴影渲染的开销过高;视觉质量也没有到想要的位置,一堆伪影与权衡;最要紧的一条,几乎不可能用来做效果(author effects)。他还说,看着 Martin 与 Asger 做出来的酷东西交付不出去,他能感到两人越来越失望。最后两条打破了他的唯一黄金法则(the one golden rule),工作流(workflow)令人痛苦。大约就是那时,他开始考虑是否需要一套根本不同的做法。这四条的分量并不平均,前两条是技术账、后两条是人的账,而他自己把权重放在了后两条。换一套技术也许能修好前两条,但只要艺术家用不了,修好也没有意义。
p33 是回到画板(back to the drawing board)的过程。最重要的一条经验是,没有基于采样的渲染器(sampling-based renderer)对艺术家限制太大。于是他重新看 raymarching,并回头核对自己当初为什么放弃它,p33 列了三条原因。其一,矩的生成与散射解算两趟 raymarch 太贵,但不做这两趟,透明与雾的集成又没解决。其二,重叠体积(overlapping volumes)必须以高效的方式处理。其三,阴影二级光线(secondary rays)的开销,这一条大致已经解决,理论上靠体积阴影贴图(volumetric shadow maps)。三条摆在一起,第三条有现成答案,第二条在离线渲染里能找到思路,只有第一条是死结,两趟 raymarch 的成本压不下去。他还说,当时最大的问题是他心里有大量怀疑(doubt),第一次尝试已经花掉两个半月(2 ½ months)。p34 的页内文字补了半句,他不想成为那个把工作扔掉、第二次尝试再失败的人,他需要一些确定性(certainty)。
运气在一周后出现。p35 说,他与家人去滑雪,父母和妹妹晚上 10 点上床,他每晚抓几个小时在笔记本上写原型。机器上只装了 Unity,但他只需要写几个 compute shader,Unity 够用。这段细节解释了一个结果,原型只求验证质量与性能的缩放,不追求接到哪套引擎里。
原型是一个穿过程序化球体(procedural sphere)的简单 raymarcher,加一道阴影,专门用来看质量与性能如何缩放(p36)。他看了一圈已有方案,早期就丢掉了不少。体积阴影级联(volumetric shadow cascades)因为拿不到他想要的质量;自适应体积阴影贴图(adaptive volumetric shadow maps)因为在内存与性能上听起来太贵。最有希望的是 Hillaire 的 Beer Shadow Maps,很便宜、内存需求也低,他试过,可这类方法假设体积密度均匀(uniform density),自阴影的质量不够。最后决定阴影还是用 MBOIT 风格的矩。理由是好质量与成本的混合,而且带着前面提过的好性质。把这几条放在一起看,便宜的方案在质量上让了步,质量够的方案在成本上让了步,而矩式阴影两边都过得去。他补了一句关键判断,在粒子那套里遇到的质量与性能问题,是因为多尺度渲染(multi-scale rendering),技术本身其实运转得很好。
p38 给出性能那一侧的消息。即使在一台 3050Ti 的笔记本上渲染阴影也「便宜得很」(dirt cheap),主视图(main view)的性能也很好;他还知道后面有很多优化空间。这个原型相比前一套技术已是巨大跃升,给了他信心,于是度假回来后的那个周一就做出决定,把既有技术留在身后。p39 补了团队那一侧,最要紧的是这让 Martin 兴奋起来(got Martin hyped),在上一套尝试卡住之后,这是急需的能量。
这个判断之所以能站住,是因为阴影的模糊是先天属性,分辨率与步长上让出去的精度不会被眼睛抓到;同一招用在主视图上会立刻穿帮,第 11 章那轮降分辨率的尝试会证明这一点。也就是说,这次顿悟救的是阴影这一半,主视图那一半的账要等到后面才算。
本章可以核对的结论是转折的判定点有两处。一处是技术判断,阴影沿用 MBOIT 风格的矩、同时改用低分辨率与大步长(p36 到 p37)。另一处是说服团队的直接证据,3050Ti 笔记本上的原型(p38),讲者给出的定性是阴影渲染极便宜、主视图性能也好,具体帧数讲者未给数,读者只能采信他的定性描述。
9在 Glacier 里成形:从基础光照循环到阴影采样
上一章结尾是那个周一的决定,既有技术被留在身后。本章 p40 到 p50 讲 Smolder 进入 Glacier 之后的第一段成形过程,主线落在光照与阴影这两件事上。
p40 是起点,讲者先把球体搬进引擎、把基础设置跑通;p41 紧接着加上基础光照循环(light loop)与阴影采样(shadow sampling)。p42 还接了一批别的光照特性,其中投影贴图(projector maps)被单独点了名,007 First Light 的美术很喜欢用,后来成了一个关键特性。这一页给了一个能核对的读数,游戏里某个酒店区域总共有 1500 盏以上的灯,其中 1000 盏以上使用投影纹理(projector textures)。这两个数字说明局部光源那一侧的规模已经落在四位数上。
p43 是体积聚光灯阴影(volumetric spot light shadows)的早期测试,也是阴影参数定型的地方。最终定下来的是 4 个矩、16 位量化(16 bit quantization)。这两个数合成每 texel 位宽:
讲者对这个数字的评价是「不算便宜」,但在阴影分辨率上可以做很多让步补回来。这一页用的是 128×128,他们支持的分辨率从低端的 64×64 一直到 1k。把它与第 6 章透明那一侧的 6 个 32 位矩放在一起,两处的存储策略就清楚了,透明用高精度少数量级,阴影用低精度加低分辨率。同一套矩表示在两个场景里取了不同的档位,取舍的依据是各自有多糊。
p44 回答了两个问题。一个是透明表面(transparent surfaces)上怎么采样这些阴影,画面里能看到阴影随体积深度累积,讲者顺带说明排序(sorting)这时还不能用。另一个是采样成本。矩式阴影在这里又给了一次机会,0 号矩(0th moment)装的是光线末端的光学深度,可以先采它,按取值判断是否落在阴影里,这样就能跳过其余矩的采样与透射率函数的求值。另外,体积阴影里不存深度,也不需要昂贵的 PCF 过滤,一次双线性采样(bilinear sample)就够。这一步顺带说明矩表示的用处不只在生成,也在采样,0 号矩正好把「在不在阴影里」这个判断单独拎出来,其余矩只在需要时才求值。这一页给了一个可核对读数,在 007 里,对同时带两类阴影的灯来说,体积阴影的采样成本大约是常规阴影采样成本的 30%。
p45 是一句顺带的好消息,因为不再有多尺度那套做法,深度测试(depth testing)也按预期工作了。对照第 7 章那句「一直没找到好解法」,这一页是同一处伪影被消掉的位置,第一次尝试与第二次尝试在这里正好形成一组对照。
p46 讲全向光阴影(omni light shadows)。它在光栅化管线里通常是大麻烦,但在 raymarch 阴影这里不受固定投影(fixed projection)限制,可以直接一趟渲进一张八面体纹理(octahedral texture)。讲者说视觉效果从这里开始有点启发,虽然此时手里还只有程序化球体,他们已经做出了一些游戏里不常见的效果。p47 把阴影采样加进了体积雾,也是在这一页,他开始把技术拿给引擎团队更多人看,兴奋感在积累。讲者给了进展速度的一个侧面读数,到这一步新增一个特性只要几小时,这说明接口已经稳定下来。p48 是一段带更高频阴影细节的压力测试。
p49 与 p50 是这一段的时间与来历。这段早期工作在 2025 年 2 月,当时完全没计划在 007 里用它。转机是烟雾弹(smoke gadget)。旧版本在视觉与性能上都有问题,最要紧的是玩法总监(gameplay director)不喜欢体积形状难以辨认;这个形状要被用于 AI 视线(line-of-sight)计算,所以在潜行玩法里清晰的视觉呈现极其重要。看到 Smolder 的早期结果后,讨论很快转向用它替掉烟雾弹。
p50 把处境说得更紧。这时是 3 月初,007 First Light 处于全面生产;揭示预告片(reveal trailer)定在 3 个月后,完整玩法公开在索尼的 State of Play,再往后 3 个月,发售还有一年。而 Smolder 远未就绪,还只是个漂浮的球体,未知项很多,把实时流体模拟做到可以发货又是另一大问题,他也没法把 Smolder 变成全职项目。最后他做的决定是,为那个烟雾弹把 Smolder 建出来,但不建在流体模拟技术上;模拟离线跑,运行时用预烘焙的密度数据(prebaked density data)。这个决定同时换掉了数据的来源与风险的来源,密度数据的管线要重新做一遍,换来的是不再被实时流体模拟的发货风险拖住。
本章可以核对的结论有两组。一组是阴影的参数与成本,4 个矩、16 位量化、每 texel 80 位,分辨率覆盖 64×64 到 1k,对同时带两类阴影的灯,体积阴影采样成本约为常规阴影的 30%(p43 到 p44)。另一组是需求侧的规模,某个酒店区域 1500 盏以上的灯里有 1000 盏以上用投影纹理(p42)。
10数据这一半:烘焙格式与运行时格式
上一章末尾那个决定把整件事推到了数据上,模拟离线跑,运行时读预烘焙的密度。本章 p51 到 p64 全在讲这半边的格式与压缩。
p51 先给选型。密度数据这条路上,最好的选择是 OpenVDB 格式。讲者说他早前在一个小型离线渲染器项目里试过,对它有偏爱(a pretty big fan),理由是它带着一套成熟的 C++ 库,到这时已经久经考验(battle tested)。p52 补的是更要紧的一条,工作流。它被广泛支持,美术可以用自己想要的工具,很多他们熟悉的建模与程序化工具都支持它。讲者给了一个能核对的来源口径,他们大部分的体积是在 EmberGen 与 Houdini 里做的。
p53 是这套选型的转折。OpenVDB 在 GPU 侧不太好用,需要一套运行时格式(runtime format)。最现成的选择是 NanoVDB,讲者说他没有经验,但看起来容易搭起来试,还专门谢了迪士尼放出云数据集,尤其是一个 1/16 缩放的版本。NanoVDB 基本上是 OpenVDB 里稀疏体素树(sparse voxel tree)的扁平化版本,上面还加了一些运行时特性,比如 fp4。结果是性能没做出来,主要原因是寄存器压力(register pressure)高。于是他们决定自研运行时格式,讲者给这笔账的口径是,多花了开发时间,但省下很多性能,也换来更多灵活性。
接下来是格式本身,从 p54 到 p56 逐层讲。资源头(resource header)装的是关于资源的通用信息,不是 Smolder 专用的(p54)。Smolder 头(Smolder header)装动画的帧数、体素尺寸,以及整个动画的包围盒;讲者特别说明这个包围盒只用于编辑器里显示边界,不用于实际渲染(p55)。再往后是一列帧头(frame headers)与一列帧数据(frame data)。帧头基本就是在描述每块数据在数据块(data blob)里的位置,它还装该帧的紧包围盒(tight bounding box),这一个才用于渲染器里的边界检查(p56)。这层做法的用意是把「整段动画的大范围」与「单帧的实际范围」分开,渲染时读后者,编辑器读前者。
p57 才是数据本身。数据存成一级稀疏 brick 网格(one level, sparse grid of bricks),每个 brick 是 4×4×4 体素。间接索引(indirection index)用 16 位。讲者说他当时觉得够用,但美术有几次撞到了这个上限,将来可能得升到 32 位。这是一处讲者自陈的余量,第 12 章把它记成一条边界。
p58 是 brick 的位掩码(bitmask),专门用于空白区域跳过(empty space skipping)。每个 brick 在位掩码里占一位,按 4×4×2 个 brick 一组来存。这里借用了快速体素遍历算法(fast voxel traversal algorithm),讲者说这算法快 40 年了,仍然好用;遍历这些 brick 组时,只要检查是不是 0 就能整组跳过。这个位掩码相比逐条走间接索引,能明显加快剔除(culling)。
p59 与 p60 是 GI 遮挡网格(GI occlusion grid)。它是相当晚才加进格式的,装极低分辨率的预烘焙遮挡,数据由 brick 块周围的均匀随机采样(uniform random samples)生成。需要它的原因是,加上 GI 采样之后体积明显偏平(noticeably flat volumes)。p60 给了一张只看 GI 的视图,讲者说即使分辨率这么低,也能给出他们想要的体积内部变暗(darkening),而且成本很小。
p61 到 p63 是 brick 数据与压缩。与 NanoVDB 的 fp4 压缩类似,他给 brick 也加了一种很简单的块压缩(block compression),元数据(metadata)里存每个 brick 的最小值与最大值(p61)。
p62 是密度压缩的结论页。他们最终用每体素 4 位(4 bits per voxel),对密度数据来说基本上没有质量损失。他也试过 2 位,结果会在阴影里丢掉一些细节,质量明显变差;讲者的定性是「不算不能用,也许将来能当一个缩放选项(scaling option)」。这一步只有定性说法,没有对比读数,第 12 章把它记成一条边界。
p63 解释了一个容易看错的数字。brick 在数据里其实是 5×5×5 体素,因为在同一个 brick 内支持运行时三线性插值(trilinear interpolation)需要复制体素。代价是 brick 需要的内存多很多,讲者认为这个权衡划算(the tradeoff is worth it)。4×4×4 与 5×5×5 两个尺寸同时出现在这一节里,前者是逻辑网格、后者是存储网格,读数据布局时不能混用。
p64 收尾讲结果。运行时格式搞定之后,他们就有了让美术开始做内容的东西。他把 Smolder 在 007 First Light 环境里的第一段测试发到游戏团队的聊天里,让很多美术很兴奋,也是在这里,Smolder 不再只是烟雾弹技术。第二天他与技术美术总监开会,对方问他能不能在玩法预告片里用 Smolder 做体积云(volumetric clouds)。
本章可以核对的结论是一串格式参数与两处自陈余量。参数是 4×4×4 的 brick 结构、16 位间接索引、4×4×2 的位掩码分组、每体素 4 位、数据里 5×5×5(p57 到 p63)。余量有两处,一处是 16 位间接索引被美术撞到上限(p57),一处是 2 位压缩会掉质量、但也许能当缩放选项(p62)。
11排序与剔除、最终集成管线、以及性能优化
上一章把数据这一半讲完,本章 p68 到 p101 讲运行时那一半:重叠体积的排序与剔除、集成管线、三轮性能优化。
先解重叠体积。p68 说,为避免逐采样检查(per sample checks),他提前算好重叠。因为 Smolder 建在线程组相干性(threadgroup coherency)上,这个计算能按组做,一个线程组(threadgroup)是 8×4 或 8×8 像素,取决于硬件是否支持 Wave64。他把光线切成段(segments):每块重叠体积给总数加两段,最后一段永远不重叠、可以跳过;007 里他把重叠体积总数限制在 4。把所有交点距离排序得到各段边界,再为每段生成一个位掩码(bitmask),raymarch 时按所在段就知道该评估哪些体积。
排序用 cutesort,一种靠 WaveActiveBallot 在 wave 内排序的秩排序(rank sort),占用位掩码用来把边界收紧(p69 到 p70)。
p71 到 p74 是这套剔除在引擎里的实物结果:三块彼此咬合的体积先搭进场景,切到调试视图,段数压到最省、边界贴得极紧,再逐段翻深度,最后跑一段早期测试,中央第一次出现几种颜色互相渗透的混合。
集成管线那一段从 p75 讲起,起点是一个理论上的干净解:三类介质都能看成透射率函数,只要各生成一份就能拼出统一函数,让三个系统各走一趟独立 pass 还完美集成,条件是把每一趟都拆成两半。Smolder 做不到,raymarch 跑两遍太贵。p76 的转念是不必全拆,让其中一个单趟跑在另外两个之间,Smolder 更贵,正好当那个单趟系统(p75 到 p76)。
p78 的答案是这套方案没有被采用,原话是生产现实又打回来了(Production realities hit again)。理由叠在一起,大改管线在生产末期风险太大;他的时间另有去处,九月那段预告片跑在 PS5 Pro 上却极其卡顿;模糊与折射管线(blur and refraction pipeline)支持太多复杂设置,维护负担极重、无法与别的系统集成;按矩求值透射率本身不贵,但对每个 raymarch 步都做就会累积,给每个采样加约 20%。
p79 到 p80 是落地的妥协版管线。前面三个前置 pass 照旧;然后进 Smolder 这一趟,raymarch 期间把主光线的累积矩存在局部变量里,同时存下 Smolder 比较不透明的那个深度,用逐像素蓝噪声让这个偏移在像素之间、帧与帧之间变化。此后几步先把体积雾撤到那个集成深度,做法是撤掉到那一点为止的散射再除以该深度的透射率,然后把 Smolder 的透射率应用到背景上,再把先前撤掉的雾散射与透射率加回结果。讲者承认这里引入了一个误差,p81 里他说自己「非常讨厌它」。
p83 到 p89 是几件收尾工作。GI 采样限制为每块体积 1 个采样位置,预烘焙遮挡帮忙卖效果(p83)。光剔除切到分簇光照(clustered lighting),核心是上下文相关的位掩码,例子里的结果是每个簇剩 7 到 8 盏灯(p84)。体积阴影的剔除剔掉所有不与 Smolder 体积重叠的灯,阴影数降到多数区域 10 到 20 个(p85);它们跑在异步计算里,用的是本来闲置的 GPU 资源,讲者说这让它们基本免费(p88)。
性能优化有三轮,起点是 TAA(时域抗锯齿)关系。Smolder 的目标是内部不做时域滤波,但整帧上仍有 TAA,用蓝噪声按像素偏移采样深度能清掉结构性伪影(structural artefacts),代价是缺少运动矢量带来的鬼影(ghosting)(p89 到 p90)。剩下的主要问题是采样数随投影矩阵(projection matrix)缩放,成本随屏幕覆盖增长(p91),他本来留了 3 周,最后变成两个周六。
第一轮把靠近相机的体积降分辨率渲染,实现容易,但空间上采样(spatial upsampling)结果不好,还破坏了 TAA 需要的逐像素噪声规则,导致极重的鬼影(p92)。第二轮换思路,抓帧显示采样成本大头在光照计算、密度查找只占小部分,于是改为降低光照求值的保真度:在 wave 内跟踪一个光照采样率(sampling rate),透射率落到阈值以下就降到半分辨率,组内线程各取 1 个采样但换到不同深度,采样存进 LDS 后继续逐像素 raymarch,降到四分之一分辨率时同理(p93 到 p94)。
这一轮仍被 TAA 挡住,体积深处也会出现大量噪声与鬼影,要压住它阈值就得低到不省性能(p96)。第三轮是 p97 的修正:仍先把采样存进 LDS,但一次在两个采样之间插值、用逐像素蓝噪声抖动系数;讲者给的读数是允许更激进的阈值,在烟雾弹的重载场景里大约省下 50% 的性能。
最后是内存,p100 到 p101。因为只能整段动画一起载入,Smolder 在某些情况下总共要用掉几百 MB,大主机不成问题,Series S 上不行。他的办法是在打包器(packer)里生成一份半分辨率体积,把每段动画的内存占用减少大约 80% 到 85%。
本章可核对的结论:8×4 或 8×8 的线程组、每段 8 个 16 位深度值、重叠体积上限 4、每簇 7 到 8 盏灯、体积阴影 10 到 20 个(p68 到 p85);按矩求值透射率给每个采样加约 20%(p78),蓝噪声插值省下约 50%(p97),Series S 半分辨率省内存 80% 到 85%(p100)。
12边界与反例(含作者自陈缺口)
上一章把运行时那一半讲完,本章收口,把作者自己交代的边界、反例与缺口摆清楚。材料来自 p31、p78、p90 到 p110 这一带,以及 p57 与 p62 两页格式说明,下面八条各给可核对的页码与数字。
边界 1:第一次尝试整体失败,终止原因只有叙事口径。 从 p26 到 p31,这条路线的形状是先给承诺、再被两处硬伤挡住;p32 的四条判定是讲者自己的复盘,分别是太复杂、性能没按预期扩展、视觉质量不足、几乎无法用来做效果,其中他说阴影渲染开销「过高」。终止这件事,讲者用的是一句「工作流令人痛苦」,p34 的页内文字只补了半句「他需要一些确定性」。他没有给出量化复盘,例如多尺度渲染到底吃掉多少帧时间。第一次失败在本篇里是一段叙事,不是一份可以复算的账。
边界 2:第二次失败是理论上更干净的方案被生产现实劝退。 p75 到 p76 那套三趟拆分在理论上能让三个系统完美集成,p78 的答案是并没有采用,理由是四条的叠加,大改管线的风险、时间被别处占用、模糊与折射管线无法集成、以及按矩求值透射率给每个采样增加约 20%。这一处可核对的只有一个数字,就是那 20%;其余三条是处境描述,读者无法判断它们各自占多少分量。
边界 3:三档硬件的读数不能直接比较。 p106 给出的是一组 PS5 等效画质设置下的用例,讲者明确说明这些数字不含异步阴影渲染的成本;p107 说 AMD 的卡在 Smolder 上「远超其重量级」表现,原因在于它们跑在 wave64 模式下,他还说这提示 32 尺寸 wave 的卡上还有很大空间可挖;p108 说 5060Ti 上性能随采样数(这里就是渲染像素数)呈现极线性的缩放。三页分别对应 PS5 等效设定、AMD 卡与主机、一张 5060Ti,测试场景没有统一,跨页做减法没有意义;p106 那组读数还缺异步阴影成本,当成完整帧开销会低估。
边界 4:TAA 与体积互相折磨,三轮优化都在这里打转。 p89 与 p90 交代关系,Smolder 自己的目标是内部不做时域滤波,但整帧上有 TAA,蓝噪声抖动采样深度换来干净的结构,代价是缺少运动矢量导致鬼影。三套 TAA 各有处置,FSR 3 必须接到反应掩码(reactive mask)输入上,否则鬼影很重;DLSS 没有对应的输入,讲者主要卡在早期 transformer 模型上,最后靠保住逐像素噪声,还删掉了把光照跑半分辨率的测试代码;升级后的 PSSR 开箱即好(p90)。后两轮优化都被同一件事挡住,降分辨率破坏逐像素噪声规则(p92),按采样率分摊光照也会在体积深处造成大量噪声与鬼影(p96)。Smolder 的最终画质有一部分由外部 TAA 决定。
边界 5:落地的集成管线是妥协品,讲者自己承认引入了误差。 p79 到 p80 的流程里,撤雾那一步是撤掉散射再除以该深度的透射率,讲者说这引入了一个误差;这套管线他自己称为「非常 hacky」,p81 里他「非常讨厌它」,只在当时的时间表下算务实解法。p110 的待办清单里也把「把 OIT 与体积雾的集成修对」列为一项。读这段时要接受一个前提,已发货的画不是理论上那套干净集成。
边界 6:压缩收益只有定性说法,没有对比读数。 p62 说每体素 4 位对密度数据「基本上没有质量损失」,2 位会在阴影里丢细节、质量明显变差,但又说「不算不能用,也许将来能当缩放选项」;p63 说数据里的 brick 是 5×5×5 而非 4×4×4,多出来的内存「划算」。这几处都只有定性措辞,没有对比读数,例如同一效果在 4 位与 2 位下的帧时间或误差。同一页还留下另一处余量,16 位间接索引已经被美术撞到上限(p57)。
边界 7:云这类效果的光照模型并不理想,系统也仍是第一版。 p103 说这套通用做法在云上给出了不错的结果,但理想情况下光照模型应当有所不同,也许留待将来研究。p109 的自我评价是,需求清单上的每一项都进了游戏,已经出现在 XBOX Series S、Steam Deck 与低端 PC 目标上,Switch 2 尚未发售;游戏里最终有 450 块以上的 Smolder 体积;但这仍然是很初版的东西,性能不错而一些场景仍需打磨。这一段把「已发货」与「已完成」分开了。
边界 8:p110 的 next todos 是作者自己列的缺口清单。 他列出的项目包括:把动画做成真正的流式加载,不再受内存限制;把实时流体模拟做进 Smolder,讲者说那会是这套技术的一次大升级;把 OIT 与体积雾的集成修对;给美术更多工具,例如颜色与自发光(emissive)支持;支持每体积独立的相位函数(phase function);远期他还想支持让美术用惯用的材质着色图(material shader graphs)设计体积材质,他自己也说这一条可能过于激进(a bit too ambitious)。另外 p106 提到他有 20 条以上与性能相关的待办想法。这组条目是最直接的作者自陈缺口。
本章可核对的结论有两条。其一,本篇里凡属失败与缺口的说明几乎都是叙事或定性口径,唯一带数字的两处是 p78 的 20% 与 p106 的「20 条以上待办想法」。其二,作者的清单(p110)与自评(p109)划出了「已发货」与「已完成」的界线,其中集成管线与流式加载两项写在他自己承认的缺口里。
13贡献块
三条贡献与站内互读
一场没有论文的讲座能留下什么,取决于它把哪些东西讲到了别人抄得走的程度。Smolder 留下来的有三样,都可以搬到别的引擎里:一套把体积效果当通用系统来设计的验收顺序(先定光照与介质叠层,再谈排序与性能);一条把「粒子 → 体素网格」当降维手段的具体做法与它的失败边界;以及一份把数据格式拆成烘焙侧与运行时侧、并把压缩粒度压到每体素 4 bit 的工程样本。三样里最容易被忽略的是第一条:本讲座的顺序按「美术能怎么用」排,而不按代码结构排。
贡献 1:把体积效果从专用系统改造成通用系统的需求分解。 讲座把需求拆成三组(光照、场景集成、视觉与性能,见第 3–5 章),每组都给出生产环境里的真实数字形状。这套分解方式可以直接拿去评估任何体积系统的完备度。
贡献 2:一次完整的失败复盘。 第一次尝试(软件光栅化粒子到体素网格)走了一年多,讲者把失败证据(深度测试伪影)与终止原因都摆在 slides 上;第二次尝试(把渲染拆成三趟)也在生产现实前收回。第 12 章的边界清单把这两次的边界条件写清楚了,这是一张坑位地图。
贡献 3:烘焙格式与运行时格式的分工,以及压缩粒度的选择。 密度数据交给 OpenVDB 烘焙、运行时另立一套带 brick 位掩码与 GI 遮挡网格的格式,压缩压到每体素 4 bit、brick 为 5×5×5(第 10 章)。凡读者想仿制的部分,这一步最可操作。
站内可以接着读的几篇:想做实时全局光照与降噪的,看篇目 22(Photon Mapping 与 Forward+ 的演进)与篇目 11(DDGI);关心不靠时序历史做缓存的,看篇目 27(ORCA);同一届 SIGGRAPH 2026 Advances 的另外三讲(篇目 23 的变率光追、篇目 26 的 PSSR 升级、篇目 27 的 ORCA)与本篇构成一组生产视角的对照。
14原文出处块
原文出处与引用口径
出处:SIGGRAPH 2026 · Advances in Real-Time Rendering in Games(Part II,2026-07-21,Petree D)· 讲者 Alexander Mueller(Senior Rendering Engineer,IO Interactive)
材料:官方讲义 PDF(113 页,带讲者备注,30.5 MB;站点标注更新于 2026-08-03)
许可:讲义页内未出现版权声明与 CC 声明 → 按分区非 CC 口径处理:逐件登记、逐图署名(slide 号 + 出处 + 讲者)、声明「未修改引用」,并在构建记录标注残留风险
引用口径:本篇所有读数、页内说法与页码均出自讲者原话(逐页讲者备注),页码为 PDF 页序(从 0 起);凡讲者未给出数字处,正文均如实写为「讲者未给数」,未做任何补算或推断