"Hi everyone, thanks for the intro Natasha. My talk today is titled “Neural Light Grid”. There was some smarter subtitle initially, but then I changed it to “adventures in trying to get something useful out of ML in production”, because this talk is just about the final solution as it is about everything that I tried and didn’t work. Many people helped me along the way, at some point we had a more formal paper describing these ideas and I wanted to acknowledge Peter-Pike, Ari and Pete who worked on that too."
- 先分清两种语境:讲者要回答的是能不能用机器学习把预计算光照现代化,想达到的是当前做法在内存上吃不下那么高的分辨率(讲义 slide 9、12);预计算光照的立足点是代价前置、运行时只查表(讲义 slide 11)。
- 两次失败都记下:想法一把某区域的辐照度分布当函数、用网络拟合 1 英寸密度的光照,被那个小测试场景的烘焙 1 到 2 小时与训练约 16 小时否掉(讲义 slide 20、28);想法二学一个辐照度分布的隐空间加共享解码器,被解码器规模否掉,6 层 512 宽解一块 4×4×4 要 3 毫秒、一帧 128 块就是 300 毫秒以上(讲义 slide 29、31、34)。
- 落地方案的一句话:用正确的权重函数。重建被写成若干探针辐亮度的加权和,权重函数不求而生成,运行时压成一个 2 隐层 8 宽的小 MLP(讲义 slide 46、48、51、57)。
- 让它在生产里跑得动的那一步:把训练权重函数网络这件事本身学成一个 generator network,约 4TB 数据离线训约 18 小时、只训一次,烘焙期直接出系数(讲义 slide 62、63)。
- 口径要一起搬走:p28 是小测试场景、p79 才是真实地图;p80 的「省约 33%」以 NLG 自身内存为分母、「约 1KB/探针」是 DDGI 每探针;p81 的逐像素性能表讲者自陈其实不用,实际是重采样到纹理、测量时放大 64 倍(讲义 slide 28、79、80、81)。
1论文里的 ML 与生产里的 ML
论文里的 ML 与生产里的 ML
讲者一开口先把这场讲的定位交底(讲义 slide 0)。题目叫 Neural Light Grid;副标题原本有更聪明的写法,后来被他自己改成「在生产里试着从机器学习里拿到一点有用东西的历险记」,理由是这一场既讲最终方案,也讲他试过而没成的所有东西。这一路有不少人帮过他,早先他们曾有一篇更正式的论文描述这些想法,他想借此致谢 Peter-Pike、Ari 与 Pete。这段开场把整场的基调定了下来:他要谈的是一个被他筛剩下的方案,以及筛掉它的那些账。
讲者的身份也在同一段里交代清楚(讲义 slide 3)。他在 Activision 的 Central Technology 组,主要服务 Call of Duty 系列,帮各个工作室达成目标。他自陈这个组像个研发组,但更偏「D」那一半:他们有一点额外的灵活度、有时可以做更远一点的项目,可它并不像典型的研究组,日常生产里处处有他们的身影。这个定位解释了后文每一次取舍为什么都先问生产代价。
接下来他把当下的风气摆出来(讲义 slide 4 到 6)。近几年机器学习的应用爆发,几乎无处不在,每年有成千篇论文,今年 SIGGRAPH 上大概九成论文都带一点机器学习。他顺口描述了一遍论文里那种叙事:取一个很难的问题,选一个看起来合适的网络架构,再往里扔大量数据,用搅拌机搅一搅,就得到一个壮观的成果。他随即把话锋一转,预告后面的实情并不完全如此。
这场讲座的安排也交代了(讲义 slide 7)。他想覆盖三件事:他原本想达成什么、他试过而没成的最有意思的几件、以及他最后定下来并真正上线的那套。他不打算只讲成功路径,这一点在第 4 章与第 5 章会看得更清楚。
那么他原本想做的是什么(讲义 slide 9)。他做了很多年预计算光照,自然先在这个方向上找应用。他给自己提的大问题是:能不能用机器学习把预计算光照「现代化」。
实时全局光照这条路他并没有否认(讲义 slide 12)。这类方案确实存在、也在变得普遍,但它们都带着不低的代价;Call of Duty 跑在 60Hz,硬件跨度从移动端一直到高端 PC,这些系统在较低端的平台上并不划算。他们愿意接受预计算光照带来的一整套取舍,但想再往前走一步,比如到达当前做法在内存上吃不下那么高的分辨率。这条路能不能通,他们把希望放在现代机器学习上。
要理解这个问题为什么成立,需要先看 Call of Duty 的处境(讲义 slide 10)。这是一款第一人称动作游戏,重度依赖预计算光照,主要用 lightmap 加上几种体积表示,来给出间接光的漫反射项。预计算光照的好处在下一句里(讲义 slide 11):它用极低的运行时开销提供很高的质量,因为这份开销被前置到了开发期,重活在那时候算完并保存下来,运行时只做查表。
讲程的安排也说明了他准备把哪些东西摊开(讲义 slide 13)。他先讲试过的方案,哪些成了、哪些没成、从里面学到了什么,再讲最终落地的那一套;这两半在他的讲述里分量相当,因为听的人真正要判断的是自己那条管线会不会撞上同样的墙。他随后又补了一步(讲义 slide 14):在讲尝试之前先做一次极简的机器学习速成,好让听的人与他在同一页上。
还有一个隐含的前提要单独点出(讲义 slide 11、12)。预计算光照之所以能用很低的运行时开销给出很高的质量,靠的是把账单挪到开发期;也正因为账单挪到了开发期,任何把分辨率抬高一个档次的尝试,都会立刻在烘焙时间与内存上体现出来。他们想要的分辨率会让现有做法在内存上吃不下,这句话既解释了目标的来处,也预告了第 4 章那道墙。
把 p9 到 p12 连起来,本章能核对的结论有三条。讲者要问的问题是用机器学习把预计算光照现代化,想达到的是当前做法在内存上吃不下那么高的分辨率(讲义 slide 9、12)。预计算光照的立足点是代价前置,开发期算完、运行时只查表,因此能以很低的运行时开销给出高质量(讲义 slide 11)。实时全局光照在 60Hz、从移动端到高端 PC 这个跨度上并不划算,所以他们选择继续用预计算光照,并把更进一步的可能寄托在机器学习上(讲义 slide 12)。下一章先把网络讲到够用,再给出第一个具体想法。
2极简 ML 速成与「把辐照度当函数」
上一章停在那个大问题上,这一章先补工具(讲义 slide 14)。讲者原本打算直接讲尝试,后来决定先做一次极简的机器学习技术速成,让所有人对齐背景。他给的定义很朴素(讲义 slide 15):机器学习是人工智能里的一个领域,研究的是能从数据中学习、并泛化到没见过的数据上的算法;当下最流行的一类叫神经网络,它从真实神经元那里借了一点灵感,讲者补了一句,这个说法其实有点勉强。
网络的骨架由层组成(讲义 slide 16)。每一层吃进一个多维向量、吐出另一个;层通常带参数,也就是一批控制它行为的数值。最简单的层就是一次矩阵乘。另一类层做非线性运算,比如 ReLU,中文叫修正线性单元,说穿了就是把所有负值夹到零。讲者用两种颜色的盒子来画网络:蓝盒表示矩阵乘、盒上的数字是它的输出维度(输入维度由上一层隐含),黄盒表示非线性。他给的例子是 6 个输入元素,先乘一个矩阵、再夹零、再乘、再夹零,最后输出 3 个值。一个点积加一次非线性,通常就被叫作一个「神经元」。
接着是这些参数从哪来(讲义 slide 17)。参数从数据里推出来,这个过程叫训练。最简单的做法是收集大量训练数据,也就是网络的输入与期望输出配成的对;然后一点点调整各层的参数,让网络对给定输入产出的结果越来越接近期望。接近的程度由一个损失函数描述,比如各分量差的平方和,也就是 L2 损失。决定怎么调参数的算法通常是某种梯度下降的变体,例如 Adam;它们需要损失函数的梯度,而梯度由反向传播算出来,从最末端一路往前面的层传。
这条路的花销也交代了(讲义 slide 18)。这类计算量很大,模型越大算得越多,而且增长很快:矩阵乘的复杂度是 O(n²),把层宽翻一倍,计算量就翻四倍。训练本身又是迭代过程,要跑很多遍才收敛。因为这个开销,训练与评估通常都放到 GPU 上;周边还有大量基础设施可用,主要是 PyTorch、TensorFlow 这类 Python 库。速成到这里结束(讲义 slide 19)。
工具备齐,第一个想法登场(讲义 slide 20)。神经网络是很好的函数逼近器,哪怕相当小的网络也能以可接受的精度逼近复杂的多维函数。于是最直接的做法是:用网络去逼近某个区域里的辐亮度,也就是光照分布。把光照分布看成一个函数,给一个位置就得到一份辐亮度。具体流程是:先在那个区域里以很高的分辨率预计算光照,比如 1 英寸的密度,这个密度直接存下来完全不现实;再训练一个网络,输入一个位置、输出那份预计算光照;训练离线做,运行时着色某个像素时把实际位置喂进网络,取回光照。
最简的网络就是一个多层感知机(讲义 slide 21)。它由一串矩阵乘与夹在中间的非线性操作组成;输入先乘第一个矩阵,过非线性,再乘下一个,如此下去。这就是他前面当例子画的那个网络:吃进 6 个值,中间夹零两次,最后输出 3 个值,也就是 RGB 颜色。起步阶段网络只负责重建空间里一小块(一个体素)内的光照,输入是归一化到 −1 到 1 的 xyz 位置,外加一个查询方向。以今天的标准看这是个极小的模型;即便如此,中间那一段 128×128 的矩阵乘已经是 1.6 万次乘加,问题在这里已经隐约露头。
这套速成里也有两处他特意强调的细节。一处关于泛化(讲义 slide 15):机器学习要的是对没见过的数据也能给出合理结果,把训练数据背下来不算数,这一点决定了后面所有尝试的成败都不在训练集上衡量。另一处关于成本的增长方式(讲义 slide 18):矩阵乘是 O(n²),层宽翻倍就是四倍算力,训练又要迭代多轮才收敛,所以「把网络做大一点」这个念头在实时场景里要先过一次算术。
本章可核对的结论有三条。讲者给的定义是,机器学习研究的是能从数据学习并泛化到未见数据的算法,最流行的一类叫神经网络(讲义 slide 15)。网络由带参数的层堆成,矩阵乘加非线性是最简组合,参数由训练得到、损失用 L2、梯度由反向传播给出;矩阵乘是 O(n²),层宽翻倍即算力翻四倍,因此训练与评估通常走 GPU(讲义 slide 16 到 18)。第一个想法是把某区域的辐照度分布当函数,用网络拟合 1 英寸这种超高密度预计算的光照;最简实现是 6 个输入、128 宽、输出 RGB 的 MLP,中间一层已是 1.6 万次乘加(讲义 slide 20、21)。下一章看这个原型在画面上成不成、在机器上跑不跑得动。
3原型好看,但逐像素太贵
上一章留下一个隐忧:中间那层 128×128 的矩阵乘已是 1.6 万次乘加。这一章先看画面上成不成(讲义 slide 22)。讲者给出结果:那是他们的测试场景,画面里只演示二次光照,整体仍由 lightmap 承担,只有画面正中一块方块用了上一页那个体素,也就是离线训练出来的网络。那份超高分辨率的预计算光照被「烘」进了矩阵系数里。他的评语是:看起来相当好,细节到位,没有真正挑得出来的伪影,够格继续往下做。
紧接着就是第一个问题(讲义 slide 23)。逐像素评估根本做不到。他那次测试的具体数字没留下来,但在当时用来测的 PS4 上,这是每帧好几毫秒的量级,完全不实用。于是他改成两段式:先把网络算到一份中间表示,分辨率比逐像素低一些,再逐像素去查这份表示,这样一部分评估成本可以在像素之间摊掉。他们把它实现成虚拟化体素(virtualized volume),由 4×4×4 的 texel 页组成;着色时若访问到一个还不存在的页,就在下一帧起一个 compute 任务把这一页评估出来。这里出现一个约束:评估时还不知道表面法线,所以不能再只输出 RGB;由于处理的是漫反射光照,他改成训练网络输出球谐系数(spherical harmonics,SH)。
输出 SH 还有个坑要绕(讲义 slide 24)。直接输出 SH 系数会得到次优的结果,因为不同系数的取值范围不一样;讲者说如果你要玩类似的东西,他试下来最稳的做法是输出 9 个方向上的漫反射 RGB,再用一次矩阵乘转成 SH,这一步是双射、一对一。这些细节他都归到「以后要玩的话」的经验里。
原型在这之后跑得相当顺(讲义 slide 25)。场景被切成体素,他们对所有体素都做了烘焙加训练,再用虚拟化体素渲染。画面整体看起来不错、跑得也行;每帧填多少个 brick 是他们自己定的,于是可以做性能预算。每帧生成 128 个 brick 就足以避免 popping,在 PS4 上大约 1ms;而逐像素查询那一步变得非常便宜,只剩几次纹理查询加一次 SH 评估。
零碎问题也有(讲义 slide 26)。按体素划分场景的方式,让体素边界上出现不连续;远处的物体会生成太多 brick,于是他们引入级联体素,这又暴露出新问题:远处体素变大时会漏光,级联与级联之间也不连续。噪声来自两处(讲义 slide 27):训练没完全收敛,以及烘焙本身;讲者判断这些都能靠一些老派的工程手段解决。到这里,高分辨率光照拿到了、60Hz 也保住了,连 12 年前的硬件都能跑。他自己抛出问题:那问题在哪。
这一版里还有两处经验他顺手给了出来。一处关于输出形式(讲义 slide 24):让网络直接吐球谐系数会掉质量,因为不同系数的取值范围差得多,换成先输出 9 个方向的漫反射颜色再一次性转成 SH,效果最好,那一步是双射、一对一。另一处关于预算(讲义 slide 25):每帧填多少个 brick 是他们自己定的旋钮,填 128 个就足以压住 popping,这个旋钮的存在说明他们当时已经把它当成一件要进生产的东西在调。
级联体素那一步的代价也要一起记下(讲义 slide 26)。远处的物体本来会生成过多 brick,引入级联是自然的反应,可一旦级联里最外层的体素变大,探针就抓不住门口的细节,漏光与级联之间的不连续同时出现。讲者把这些归到「老派工程能修」的一类里(讲义 slide 27),另一部分是训练没完全收敛与烘焙本身的噪声。他这一页的自信有依据:高分辨率光照、60Hz、连十二年前的硬件都能跑;也正因为画面与性能都过关,下一页的账才显得刺眼。
本章可核对的结论有三条。原型在画面上成立,测试场景只演示二次光照,正中一块体素由离线训练的网络承担,细节到位、没有可挑的伪影(讲义 slide 22)。逐像素评估当场被否掉,PS4 上每帧好几毫秒,于是退成虚拟化体素加球谐的两段式,页大小为 4×4×4,缺页在下一帧补算(讲义 slide 23)。虚拟化体素版每帧 128 个 brick 足以避免 popping、在 PS4 上约 1ms;剩余问题是体素边界不连续、远处漏光与级联之间不连续,以及训练与烘焙带来的噪声(讲义 slide 25 到 27)。下一章算总账。
4代价翻车,转向隐空间
上一章结尾,讲者把问题留在了空中。答案是不划算(讲义 slide 28)。问题出在他们把预计算做得太猛:烘焙这一侧,他们本有一件高性能的烘焙工具,可要在 1 英寸分辨率下把那个小测试场景烘出来,在讲者 32 核的 i9 上花了 1 到 2 小时(他们在 CPU 上烘);更糟的是训练这些网络,在一张还不错的 Titan RTX 上花了大约 16 小时。而这只是一个极小的测试场景,平时它大约 20 秒就能烘完。场景里只有约 500 个体素,每个体素训练约 2 分钟,一累加就爆。他的判断是:即便把整条管线提速一个数量级,仍然慢一个数量级甚至更多。这条路明显不通。
于是他换个方向想(讲义 slide 29)。既然烘焙时间是问题的大头,那能不能用机器学习把整条管线端到端现代化,把烘焙也一起换掉。他引入表示学习(representation learning)这个概念:把数据变换并用一种多维的「隐」表示来表达,隐空间里的点对系统有内在含义,比如相似的对象会落得比较近。由此得到第二个大想法:学一个辐照度分布的隐空间,于是一个区域的光照只需用一个多维向量表示。数据并不缺,他手上有一大批光照质量很好的历代 Call of Duty 地图。做成之后,取一个体素的光照,做法就从烘焙加训练网络,变成高效地找到它的隐表示;运行时拿这个隐向量,就能生成任意点上的高分辨率辐亮度。
这个想法有现成的参照(讲义 slide 30)。2018 年 DeepMind 有一篇讲得漂亮的论文,提出生成式查询网络(Generative Query Network,GQN)。思路是:给系统一个场景与若干次观测,也就是从某些视点渲出来的图;这些观测先过一个表示网络,各自得到一个隐表示;它们再被组合起来,实际做法就是相加,形成一份聚合表示;然后由第二个网络,也就是解码器,来做新视角合成,从新位置渲出画面。这套东西妙在聚合表示是概率性的,隐空间里的一个高斯分布:系统因此对「自己对场景结构有多确定」有数,观测不足以推断清楚时方差就大;继续补观测、把新观测与旧的累积起来,就能得到方差更小的表示,而且这个过程可以迭代。
把这套搬到烘焙上就有了方案(讲义 slide 31)。假设网络已经训练好,能从若干点给出光照的隐表示:他们先在这些点上烘光照,网络给出一个隐表示;这个表示是一个高斯分布,也就是均值加方差。如果这些点足以推断辐照度分布,方差就小,到此为止;方差大的时候就多烘一些点、再走一遍流程,提供更多观测、得到更紧的分布。满意之后只保存分布的均值;运行时把它交给一个通用解码器,就能从隐表示得到任意点的辐亮度。原先每个体素一个网络,现在被换成这个体素的一份隐描述,加上一个在所有体素之间共享的解码器。
这一步的取舍也有它自己的代价(讲义 slide 31)。概率表示在这里的好处是它可以迭代,方差大就补观测、直到方差够小为止;坏处是最后保存下来的只有均值,那份「系统有多确定」的信息在落盘时被丢掉了。他也说了这套东西的野心在哪(讲义 slide 29):想法一只是把查表换成网络,想法二想连产生那份光照的过程一起现代化,把烘焙也换掉。
本章可核对的结论有三条。1 英寸分辨率下烘焙那个约 500 体素的小测试场景要 1 到 2 小时(32 核 i9、CPU 烘焙),训练网络约 16 小时(Titan RTX),而同一场景平时约 20 秒就能烘完;即便提速一个数量级仍然太慢,讲者据此否掉想法一(讲义 slide 28)。想法二借表示学习学一个辐照度分布的隐空间,让一个区域的光照缩成一个多维向量(讲义 slide 29)。它的参照是 2018 年 DeepMind 的 GQN,聚合表示是隐空间里的高斯分布、方差表达确定性、可迭代累积(讲义 slide 30);搬到烘焙上就是若干观测点给一个隐表示、方差大就多烘点,最终只存均值,配合一个全局共享的解码器(讲义 slide 31)。下一章看它实际跑出来的样子。
5隐空间也翻了,讲者回头算这笔账
想法二被实现出来之后,结果让讲者失望(讲义 slide 32)。他把那一页的效果形容成一片混乱,只有少数区域勉强像在做点合理的事,整体上说不过去。于是他开始拆(讲义 slide 33):编码器那部分工作得还行,行为符合预期;对比较简单的光照分布,比如自由空间,烘一组点就能得到低方差的表示;几何复杂的区域则需要更多样本。问题出在解码器,它给不出任何合理的输出。这时候要问的问题很多:是编码器没做对,还是解码器该换架构,还是数据不够,或者完全是别的原因;讲者强调这种局面很难有把握地判断。
症结最后落在规模上(讲义 slide 34)。他承认自己把各部分都做得太保守:始终想着要实时跑,所以解码器一直压在中等尺寸,128 宽的层,胆子大点才到 192 或 256。可要让解码器输出哪怕勉强合理的东西,它需要大得多的表达能力,要更大、要在更多数据上训练、也要更灵活。然而即便一个还算简单的 6 层、512 宽架构,解码那套虚拟化体素里的单独一块 4×4×4 brick 就要 3 毫秒;而为了视觉稳定一帧需要解 128 块,那就是 300 毫秒以上。他在这里用了一个词:糟糕。
效果也并没有随规模变好(讲义 slide 35、36)。6 层 512 宽的效果只是勉强开始像点有用的东西;换到更宽的解码器,大概是 768 或 1024 宽的层,看着稍好一些,离他想要的质量仍然很远,而解一块 4×4×4 已经要约 10 毫秒。
他在这一页上耗的时间远超预期(讲义 slide 37)。他试过更小的体素,行为确实好一些,代价是内存占用高得多、远处还有 aliasing 的问题;他试过各种解码器架构,卷积、LSTM、transformer、normalizing flows;也分析过激活层与输入编码对结果的影响。共同的规律是:只要看起来有点希望,代价就已经太高,预计算、运行时或内存,有时三者一起。于是他把这个想法放到了待办架子上。
一句牢骚之后是这场讲座最重要的一次回头(讲义 slide 38、39)。他先写下一句不甘心:这些神经网络对别人都那么好用,总得从里面挖出点什么。然后退一步,去找这些失败尝试的共同点,结论是他对它们期望太高:他想从一份被压得极紧的表示里重建复杂而细节丰富的信号。神经网络确实能生成复杂数据,看看 DALLe、Stable Diffusion 就知道,但评估这些模型的成本,相对实时能承受的范围高了好几个数量级;小模型的结果则温和得多。他举深度学习超分当例子:可管理的模型能把图像放大 2 倍,也就是 4 个像素生成 16 个,做得不错,而即便是这种模型也已经相当大。不过这一轮探索让他学到不少,所有迹象都指向同一个方向:坚持小模型,也许可以开发一个来改进他们现有的系统。
中间那两张更宽解码器的画面他也没省(讲义 slide 35、36)。6 层 512 宽的效果只是勉强开始像点东西;再往上加到 768 或 1024 宽,画面确实好看了一点,但离他要的质量仍然很远,而单块 4×4×4 的解码时间已经从 3 毫秒涨到约 10 毫秒。这条曲线说明加宽已经救不了场,表达能力上的缺口大到再往上堆宽度也补不上。
他自己总结这段弯路的方式很直接(讲义 slide 39)。他想从一份被压得极紧的表示里重建细节丰富的信号,而这件事在实时预算里做不到:DALLe、Stable Diffusion 这类模型确实能生成复杂数据,评估成本却高出好几个数量级;小模型的结果温和得多。他举深度学习超分当参照,把图像放大 2 倍、也就是 4 个像素生成 16 个,已经要用上相当大的模型。这一页把他后面的选择限定了:继续用神经表示,但只把它放在一件信号本身就很平滑的事情上。
本章可核对的结论有三条。想法二实现出来是一片混乱,编码器尚可、解码器给不出可用输出,病因定位在解码器规模不足(讲义 slide 32、33)。即便 6 层 512 宽的简单架构,解一块 4×4×4 brick 要 3 毫秒,一帧 128 块就是 300 毫秒以上;再加宽到 768 或 1024 也要约 10 毫秒一块,质量仍远不达标(讲义 slide 34 到 36)。尝试过的更小体素与各种解码器架构都逃不开同一条规律,只要有希望,预计算、运行时或内存的代价就已经太高;讲者的教训是坚持小模型(讲义 slide 37 到 39)。下一章回到引擎里已有的系统。
6回到引擎现状,把「节点」变成网络
上一章把两种大想法都试穿了,这一章回到引擎里已经跑了多年的东西(讲义 slide 40)。他先列出游戏里在用的:2D 与 3D 的 lightmap,这一件很难打败;skylight,这是他们内部给远景体积系统起的名字,作用是光照的一份低层次细节,它基于移动基分解(moving basis decomposition),Ari 几年前在 EGSR 讲过;还有 lightgrid,它是辐照度体积(irradiance volume)的一个变体,为动态物体、体积效果等提供光照,可以在空间里任意一点查询该处的辐亮度,2017 年他们在同一门课上讲过。lightgrid 基于一张连接探针的四面体网格,已经开始显老:它占用相当多内存;它靠拒绝不想要的探针来避免漏光,而那个做法有点问题,它并不满足 C0 连续性;在大地图上也不好处理。讲者一句话收拢:这确实是一件有改进空间的东西。
为什么偏偏挑它来试机器学习,讲者给了一条很实在的理由(讲义 slide 41)。lightgrid 是异步采样进 3D 纹理、再在渲染时取用的;通常一帧里会有几万次查询,用在角色、粒子、体积效果上。查询的开销只算有点重要,因为他们查得并不多。真正让机器学习有戏的是另一件事:这份信号的频谱内容低得多。反正他们要重采样,lightgrid 里并不存超高频的细节,所以它有希望用一个负担得起的模型来表示。
于是新模型成形(讲义 slide 42)。页面给的是测试地图的一个截面,俯视一块平面上的辐亮度,能看出外面亮、里面暗、光从门口穿进来;讲者说这一页能让人对要编码的信号有个概念。他们在这个平面上铺了一层节点网格,作用有点像探针。因为早前的实验都在体素边界上留下硬边,这一次他们从建模之初就把平滑插值纳进去:每个网络的职责是重建它周围的光照,输入局部位置与方向、返回该处光照;而空间里每个点会受 4 个这样的网络影响,所以评估 4 个再把结果插值起来。接下来他们用整张关卡体积的光照,去训练这一整批网络。
结果比前两次好得多(讲义 slide 43)。左边是重建、右边是参考,两者非常接近;神经网络版本能把墙边的光照不连续表现得很干净,亮光在边缘处利落地停住,没有漏光。讲者给的原因是网格密度与要逼近的信号频率匹配得多:网格更贴合信号的尺度,于是它不需要「幻觉」出那么多东西,重建自然做得更好。
到这一步还不算完工(讲义 slide 44)。他们要替换的是一个已经工作了六年多、触角伸得到处都是的系统。举一个具体的约束:他们非常依赖光照表示的线性性来做运行时合成,这一条在 2020 年的 UberBake 论文里讲过;网络是非线性的,不好合成,所以不能直接拿来当表示。讲者让人仔细看刚才那个原型:它骨子里仍然是一个辐照度体积,只不过被现代化、被机器学习加持过,有网格、要在节点之间插值,只是节点从辐照度探针换成了网络。于是真正的问题变成:怎样才能继续用辐照度探针,又拿到一部分神经表示的好处。这一页他还补了一句旁注:Pete Shirley 是他的老板,也是原始辐照度体积论文的作者之一,30 年后回到这个话题,他很高兴。
线性性这条约束要摊开说(讲义 slide 44)。他们做运行时合成,是把几份光照表示按权重叠起来,前提是每一份都满足线性叠加;网络在输出端带非线性,叠起来的顺序与结果会不一致,于是它没法像一张探针表那样被合成。这也是为什么原型再好看也只能停在原型:它在画面上成立,却在管线的接口上接不上。
本章可核对的结论有三条。引擎里现有几件光照资产,2D 与 3D lightmap、skylight、lightgrid,其中 lightgrid 是辐照度体积的变体、可作空间任意点查询,但它内存占用不小、拒绝探针的做法并不满足 C0 连续性、大图难处理(讲义 slide 40)。之所以选它来试,是因为它一帧只被查询几万次、查询开销不算关键,而它的信号频谱内容低得多,更容易用负担得起的模型表示(讲义 slide 41)。新模型让节点自己就是小网络、每点取最近 4 个插值,从建模之初带上平滑插值;重建与参考非常接近,墙边截断干净、没有漏光(讲义 slide 42、43)。真正的拦路条件是线性性,运行时合成依赖它,而网络给不了(讲义 slide 44)。下一章把重建写成一个可以求解的目标。
7把重建写成最小化问题
上一章把「节点是网络」这条路的收尾条件挑明了:表示要能被线性地合成。这一章讲者换了个角度,先处理重建本身(讲义 slide 45)。他们一直习惯把辐照度体积的重建当成在存下来的值之间做混合或插值,这既是它最初被描述的方式,也是如今最常见的做法。可这样用会有几个严重的缺点,最要紧的一条是它不关心几何,直接穿过墙去插值,于是产生漏光。业界对这些问题的处理都相当临时:往探针里塞一些可见性信息,把墙后面的探针丢掉;或者微调查找的位置。讲者说这些都容易出错,他想要的是一套更好、也更统一的方式去重建正确的光照。
方式本身很简单(讲义 slide 46)。他把重建正式写出来:给定一个域 S,想用近似辐亮度 Lhat 去逼近真值 L;Lhat 写成若干个探针辐亮度 Lp 的加权和,每一项由随空间变化的权重函数(weighting function)Phi 加权。目标是让两者之差,也就是损失,在整个域上积分最小;而要最小化它的手段,是同时解出探针里存的辐亮度与那批权重函数。在典型的辐照度体积里,权重函数就是固定的三线性插值函数。可以把可见性塞进这些函数里,他们旧系统与 DDGI 都这么做,但那并不意味着在最小化重建误差,至少不是以任何有原则的方式。正规的做法是找出能给出最优重建的那组权重函数。
再往后是方案的一句话版本(讲义 slide 48)。同一场里另有一页没有备注(讲义 slide 47)。讲者用一页 TLDR 把方案压成一句话:用正确的权重函数。他们最后做出来并上线的,是一个现代版的辐照度体积,由一批放在某种数据结构里的探针,加上它们权重函数的一份表示组成。采样光照时,先找出哪些探针影响当前采样位置,评估它们的权重函数,再按权重把结果混合起来。他强调,真正落地发货的东西,比起初设想的那个规模谦逊得多,后面几页他会把这些部件按一个略随意的顺序逐个过一遍。
权重函数怎么来,先看正规解法(讲义 slide 49)。把权重函数写成某种参数形式,就可以基于上面那个最小化问题去解。最直接的做法是把它离散化、也就是做点采样,再用某个梯度下降算法去最小化误差,最简情形下就是 L2。他们在每个探针周围生成 8192 个随机点,这个数目足以把周围空间覆盖得不错,同时还算可控。把这些函数可视化之后,行为基本符合预期:在自由空间里它们看上去就是一个简单的插值核;遇到墙这类边界,就干脆地停在墙上;小的障碍物基本被忽略,大的障碍物则被平滑地绕过去;自由空间里它们是连续的。求解是「正确」的做法,但它是一个全局问题:所有函数互相重叠、互相影响,必须一起解,于是形成一个可能非常大的系统,也就很慢,慢到生产上不能接受。
于是换成生成(讲义 slide 50):放弃求解,改为一个个完全独立地生成,但生成出的结果要与求解得到的相似。
两页之间还夹着一页没有备注(讲义 slide 47),正文不再从它取信息。要留意的是形式化那一步里两个量的分工(讲义 slide 46):探针里存的辐亮度和那组随空间变化的权重函数要一起求出来,前者回答每个探针看到的是什么,后者回答每个探针对某个位置负多少责。传统做法是前者求、后者写死;DDGI 一类的做法把可见性塞进后者,那只是给权重函数换了个来源,并没有把它放进最小化目标里。把这两件事分开看,就能明白为什么这一页被讲者当作整篇的转折点。
本章可核对的结论有三条。传统把辐照度体积的重建当成插值,它的代价是不看几何、穿墙插值、于是漏光,而业界的补丁都偏向临时手段(讲义 slide 45)。正规写法是把重建写成「若干探针辐亮度的加权和」,在域上最小化与真值之差,要解的量是探针辐亮度与权重函数两件;把权重函数固定成三线性插值的做法并不构成对重建误差的最小化(讲义 slide 46)。用梯度下降求解是正确路径,但它是全局问题、系统可能很大、生产上太慢,因此他们改成一个个独立生成(讲义 slide 49、50)。下一章讲生成的具体过程。
8生成权重函数,并把光照在 support 上积分
上一章把求解与生成的分岔点立在那里,这一章讲生成怎么做。讲者的比喻是体积散射(讲义 slide 51)。过程从探针中心出发:所有从中心可见的离散点,都拿到一个等于三线性插值权重的初值;接着跑两趟「扩散」pass,每个还没收到任何贡献的点,会去看它最近的邻居、把邻居的权重收集过来,中间配一个人手调的高斯衰减。他用可视化解释这件事:为了便于分析,只画某个平面上的离散点。第一步得到一个以中心为圆心的径向模式,但右后方被一段墙挡住;接下来两步里,这个函数逐渐「爬」过那道边,并慢慢变平滑。
生成与求解到底差多少,他也看了(讲义 slide 52、53)。他把两边摆在一起对比,求解得到的那些放在左边、生成的放在右边。结论是它们并不逐点相同,可行为非常相似。
生成要为每对点算可见性,这件事差点成为瓶颈(讲义 slide 55)。每个探针有 8000 多个点,而一张地图上通常有几十万个探针,朴素地去测这些连接会慢到不可行。他们的加速手段有几条:所有探针共用同一套点模式,于是有些东西可以预计算;每个点只分析到最近 32 个邻居的可见性。讲者特意说明,这些数字只是实验的结果,是表示质量与计算成本之间的某种平衡。即便如此,连接仍有 26 万条以上,于是他们把连接分组、放进一棵 BVH;对每个探针,先拿 BVH 对场景做一次 BVH 级的测试,快速丢弃那些与场景几何完全不相交的大组连接,只有可能撞上东西的连接才真的走常规射线求交。这样一套下来,一个约 20 万探针的典型场景,全部相交计算不到一分钟。
探针里存的光照同样不求解(讲义 slide 56)。理由与权重函数一样,所以改成在权重函数的支撑域(support)上做积分。做法是先为支撑域里的一个子集烘一份低质量光照,这个子集是 256 个点;然后利用点与点之间的互通可见性,把光照传播到每个点的可见邻居;等每个点都有了自己的光照,再乘上权重函数的值、把全部贡献加起来。这一步相对「直接在探针位置渲一张 cubemap」那样的点采样有个很大的好处:它把辐亮度信号在探针的支撑域上做了空间滤波。这消掉了点采样辐亮度与可见性带来的一类显著空间 aliasing,它的表现是探针辐亮度对位置特别敏感,探针位置稍微一动,它周围重建出来的信号就会大幅变化。
扩散为什么能替代求解,讲者给的解释落在几何上(讲义 slide 51)。第一步只让从探针中心可见的点拿到权重,于是墙后的空间拿不到任何初值;两趟扩散里,权重沿着互相看得见的邻居关系往墙后蔓延,但每蔓延一步都被高斯衰减削弱,于是它绕过墙角、却不会直接穿过墙。这个行为与他上一章可视化里绕过大障碍、忽略小障碍的观察对得上,也是生成结果能与求解结果相似的原因。他也把两边摆在一起比对过(讲义 slide 52、53):不逐点相同,行为非常相似。
这套生成要依赖一份点与点之间的可见性表(讲义 slide 55)。每个探针有八千多个点,而一张地图上探针是几十万级,连接数是平方级的;他们靠共用点模式、只看最近 32 个邻居、再用 BVH 做分组剔除,把这件事压到不到一分钟。讲者对这些数目的态度很一致:它们都是实验里试出来的平衡,并非推导出来的常数。探针光照那一侧也一样(讲义 slide 56),256 个采样点同样只是他试下来合适的值。
本章可核对的结论有三条。生成过程是先给可见点三线性权重,再做两趟带高斯衰减的扩散,结果是函数能越过遮挡边缘并变平滑,与求解得到的行为相似、但不逐点相同(讲义 slide 51 到 53)。可见性计算靠共用点模式、只看最近 32 个邻居、把 26 万条以上连接分组进 BVH 做层级剔除,于是约 20 万探针的场景能在不到一分钟算完;这些数值都是讲者所述的实验取值(讲义 slide 55)。探针光照也不求解,改成在支撑域上积分:先给 256 个支撑点烘低质量光照,再传播给可见邻居、按权重求和,这样把辐亮度在支撑域上做了空间滤波,消掉了点采样带来的显著空间 aliasing(讲义 slide 56)。下一章把这些函数送进运行时。
9运行时的小 MLP 与探针的层级
上一章结束时,权重函数还是一堆离散点,这一章讲它怎么进到运行时(讲义 slide 57)。他们把这份点的表示转成一个极小的神经网络:输入是局部 xyz 位置,两个隐层、8 宽,激活用 LeakyReLU,输出一个值,也就是该位置的权重。讲者顺带解释 LeakyReLU:它是一类不同的非线性激活函数,与 ReLU 相似,只是不把负值夹掉,改成乘上一个小数;最简单的说法是它能帮着训练,也给网络多一点表达能力。训练这些网络时,他们自己写了 Adam 与反向传播的实现,原因是 PyTorch 在这种小而受限的场景里太慢:他们的测试里,PyTorch 的 CPU 版本可能比自写代码慢约 100 倍,而 GPU 版 PyTorch 只比自写的 CPU 代码快 15%,这些都在 AMD 5995WX 加 RTX 3090 上测的。
过程里的细节也交代了两处。第一处关于落进几何内部的支撑点(讲义 slide 58):很多点会落在几何体里面,讲者说这其实是好事,因为它们不需要在优化时贡献损失,网络在这些区域里想做什么都行,训练因此高效得多;这份「有效与否」的信息来自他们的烘焙管线,是光照计算的一个副产品。第二处关于探针自己落在几何内部(讲义 slide 59):那时候按常规生成的权重函数是错的,会整片空掉。他们的处理完全落在同一套框架里:先给所有支撑点生成光照,在 RGB 空间做聚类,把亮度相近的区域归到一起,这样万一有硬的不连续,生成出的权重函数也不会渗过去;接着取最大的一簇,再在三维空间里找它的连通分量,因为 RGB 上连续的一块在空间上可能是断开的;取最大连通分量,其中每个点拿到一个等于三线性核的初始权重,之后照旧做空间扩散。讲者强调这种事不需要微调探针,也不需要任何 hack,它自然就落进了整套框架。
探针本身也有组织方式(讲义 slide 60)。它是一棵比较标准的树,每个轴上的细分因子是 4,也就是每个节点最多 64 个子节点,树最多 3 层深;每个节点里存一份 payload,是一 brick 探针,可以是 2×2×2(探针只在角上)、3×3×3(多一个中心探针)或者 5×5×5;相邻节点之间共享边与角上的探针。
到这里系统的其余部分都齐了,可还有一道坎(讲义 slide 61)。即便优化器已经很快,为每一个权重函数都优化一遍网络仍然太慢:讲者的 Threadripper 每秒大约能处理 60 到 80 个权重函数,对一张中等大小的地图就是大约一小时,他觉得这有点慢。他接着换了个念头:把整个训练过程看成一个函数,输入是权重函数、输出是网络系数。
几何内部那两种情形他讲得比参数更细(讲义 slide 58、59)。支撑点落进几何体里是常态,讲者把这件事当成优势用:这些点不参与损失,网络可以在里面自由取值,把优化容量省给真正看得见的地方;有没有落进去由烘焙管线顺手给出,不需要额外判断。探针自己掉进几何体里则要另作处理,它的权重函数按常规生成会整片空掉;他们的办法是先在 RGB 空间把支撑点按亮度聚成簇,再在三维空间里取最大的一簇,只让这一簇拿到等于三线性核的初值,然后照旧扩散。这一套没有引入新的数据结构,也没有把探针搬走。
探针的存储方式也决定了查找的代价(讲义 slide 60)。树在每个轴上按 4 细分,最多三层深,节点里放的是一格 2×2×2、3×3×3 或 5×5×5 的探针,相邻节点共享边和角上的那些。共享这一条让格子之间不会出现缝,代价是查询时要把共享的探针去重;三种 brick 规格则对应不同尺度上对密度的需求。
本章可核对的结论有三条。运行时的权重函数表示是一个 2 隐层、8 宽、LeakyReLU 激活的小 MLP,输出单个权重值;训练它需要自写的 Adam 与反向传播,因为在这种小而受限的场景里,PyTorch 的 CPU 版约慢 100 倍、GPU 版也只比自写 CPU 代码快 15%(讲者所述,测于 AMD 5995WX 加 RTX 3090)(讲义 slide 57)。落进几何内部的支撑点不参与优化、探针落在几何内部时改用 RGB 聚类加三维连通分量重做初始权重,两种情况都不需要调整探针位置(讲义 slide 58、59)。探针按 4 叉树加 brick 组织,每轴细分因子 4、树最多 3 层、brick 为 2×2×2 或 3×3×3 或 5×5×5,相邻节点共享边角(讲义 slide 60)。剩下的一处瓶颈是逐函数训练太慢,Threadripper 每秒 60 到 80 个,中等地图约一小时(讲者所述)(讲义 slide 61)。下一章把训练本身也学掉。
10generator network:把训练本身再学一遍
上一章末尾那个念头在下一句里就成立了(讲义 slide 62):神经网络正擅长逼近这类函数。于是他们训练了另一个网络,用来逼近训练过程本身。它接过生成出来的权重函数作为输入,直接、不做任何迭代,就产出近似那份权重函数的网络系数。他们把它叫作 generator network,它属于 hypernetwork 的一种,也就是 meta learning 的一种;讲者在这里补了一句玩笑,至少他们自己是这么认为的,有些人很坚决地说它不是。
先看它吃什么、怎么训(讲义 slide 63)。他们拿了一批地图、生成权重函数,再把东西随机移位、生成更多,合计大约 4TB 的训练数据。这个网络完全离线训练,这次用 PyTorch 在 GPU 上跑,训到收敛大约 18 小时;讲者说这不成问题,因为它只需要训一次。训完把它从 PyTorch 里导出,在烘焙时使用:光照师烘一张地图时,每个探针都会评估一次。他们还写了专门的 SSE 代码,利用这个网络已知的结构,把推理时间再省下几秒。
网络本身什么样,讲者也报了(讲义 slide 64)。它是一个 6 层的感知机,用 LeakyReLU 激活,还带 skip connection。他特意说架构其实不太关键,这个网络对改动相当鲁棒;这套设置命中了他们的甜蜜点,再往大做只有很小的改善,于是就停在这里。输入是权重函数的主成分分析(PCA)投影,只取前 512 个系数;PCA 的基由整个训练集导出,这样就限制了输入的尺寸,按讲者的说法,这是评估时间与质量之间的一处平衡。
它并不万能,但缺口可以补(讲义 slide 65)。generator network 基本上消除了大多数函数还需要训练这件事:把权重函数放进去,另一端就能拿到系数。不过也有结果不理想的情形,多半是因为训练数据里某类函数太少;他给的例子里,初始输出会轻微地穿墙漏光。这不成问题,把 generator 给出的结果当作起点,继续跑常规优化,训练一小会儿就好了。整条链路最后被画成一张总图(讲义 slide 66):从权重函数出发,经过 generator network 得到第一遍的表示,需要的时候再进一步精化。
把这件事做成超网络,解决的是上一章结尾那道算术(讲义 slide 61、62)。逐函数训练在讲者的机器上每秒只能过 60 到 80 个,一张中等地图就要一小时,地图一多就顶不住;改成生成之后,同一台机器每秒能过约 6 万个探针,约差一千倍。讲者对这算不算 meta learning 这个问题很放松:他们把它当成 hypernetwork、当成 meta learning 的一种,也有人很坚决地不同意,他并没有把结论钉死。
网络的结构与输入也交代得比一般论文更省(讲义 slide 64)。6 层感知机、LeakyReLU 加 skip connection,再往上加宽几乎没有收益;输入取权重函数 PCA 投影的前 512 个系数,PCA 的基由整个训练集导出,这是为了把输入尺寸压住。讲者给的理由是要在评估时间与质量之间取一个平衡,并非这个结构本身有什么特别之处。他也补了一句:这套网络对改动相当鲁棒,换掉某些细节并不会让结果崩掉。
本章可核对的结论有三条。generator network 是一个逼近训练过程本身的网络,输入权重函数、输出网络系数,不做迭代;讲者把它归为 hypernetwork、也就是 meta learning 的一种(讲义 slide 62)。它用约 4TB 训练数据离线训练、约 18 小时收敛,只训一次,之后在烘焙时对每个探针评估一次,另有 SSE 代码压缩推理时间(讲者所述)(讲义 slide 63)。网络是 6 层感知机、LeakyReLU 加 skip connection,输入取 PCA 前 512 个系数;它不总是完美,输出不理想时可以接着跑常规优化(讲义 slide 64、65)。下一章看结果与读数。
11结果与两组读数
讲者说现在可以看结果了(讲义 slide 67)。第一张是那道必修的 Sponza(讲义 slide 68):这里用的是逐像素评估,即便探针密度相当低,结果依然可以接受,也没有漏光伪影。下一张是同场景的真值(讲义 slide 69),讲者的评语是质量当然好得多,但考虑到这套东西占用的内存那么少,他认为不算差。
另一道必修题是 Cornell Box(讲义 slide 70)。这一张用了 8 个探针,对比对象是 DDGI 的漫反射项。讲者解释了 DDGI 的做法:它依赖低分辨率的八面体深度图来丢弃被遮挡的探针,权重函数是隐式的、建立在那批深度缓冲之上。他要说明的是,在这么低的探针密度下,DDGI 出现了一些不连续伪影,而且用的内存更多。紧接着是误差图(讲义 slide 71):两边的量级总体相近,但 NLG 的误差变化是平滑的,讲者说这是一个重要特性。Cornell Box 再跑一遍时探针密度高得多,是 9×9×9(讲义 slide 72):这时候 NLG 与 DDGI 看上去几乎一样,但 NLG 用的内存少得多。误差图跟在后面(讲义 slide 73)。
发货地图上他也给了截图(讲义 slide 74),并且注明这些图为了演示仍旧是逐像素渲染的,而那并不是他们的实际用法;另一张地图则把探针画了出来,好让人看清密度(讲义 slide 78)。
真正支撑这套东西进生产的读数在两页上(讲义 slide 79、80)。预计算时间表覆盖了几张地图:最大的一块成本是光照计算,其次是可见性。讲者提了一处口径:他们把「函数数」与「探针数」分开报告,因为中间有一道去重 pass,会把完全相同的权重函数合并掉。他的结论是 generator network 是极高效的取得最终表示的方式,尽管它并不总是完美;在讲者那台相当强的机器上,它每秒大约能处理 6 万个探针,而直接优化网络在同一台机器上只有每秒 50 到 60 个探针。讲者说 generator network 是整套系统的关键部件,没有它在生产里就不实用;而且即便他们有一座巨大的计算农场,烘焙发生得如此频繁、需求如此之大,也只能本地跑在光照师的机器上,和其他烘焙一样。
内存那边同样有一张表(讲义 slide 80):同样这几组场景,他们通常比旧系统省大约 33%;DDGI 在内存上要贵得多,因为每个探针都得存一份唯一的 2 通道深度缓冲,合计大约 1KB。这里有一句必须照原意读的限定,讲者明确说明:这些百分比是相对 neural lightgrid 自身的内存用量算出来的。
预计算那张表里还有几处结构要指出(讲义 slide 79)。成本最大的两块是光照计算与可见性,这与前几章的流程对得上:先要有一份低质量光照铺在支撑点上,再要一次次判断点与邻居之间看不看得见。他把函数数与探针数分开报告,是因为中间那道去重 pass 会把完全相同的权重函数合并成一个,同一张地图上函数数可能小于探针数;这一点也解释了为什么后面那张速率要用探针来计。他还补了一句现场的约束:即便公司有一座巨大的计算农场,烘焙发生得太频繁、需求太大,这件事也只能放在光照师本机跑。
发货地图那两张图的作用也要分清(讲义 slide 74、78)。一张是成品的画面,讲者注明它是为了演示才逐像素渲的,实际并不这么用;另一张把探针画出来,让人看到密度到底有多低。两张图合起来说明一件事:这套表示是按够用设计的,密度与画面质量之间的余量并不宽裕,只是因为他们要重采样、用不到贴近墙的值,这份余量才够。
本章可核对的结论有四条。Sponza 在很低探针密度下用逐像素评估即可接受、没有漏光伪影,真值当然更好,但按占用内存来看差距可接受(讲义 slide 68、69)。Cornell Box 的两组对比要分开读:8 探针下 DDGI 有不连续伪影且内存更多,9×9×9 下两者看起来几乎一样、NLG 内存更少,两组误差图都显示量级相近而 NLG 更平滑(讲义 slide 70 到 73)。预计算时间表里最大成本是光照计算、其次是可见性,函数数与探针数分开报告;generator 每秒约 6 万探针、直接优化每秒 50 到 60 探针,均为讲者所述(讲义 slide 79)。同批场景内存通常比旧系统省约 33%,这个百分比的分母是 NLG 自身内存,而约 1KB 是 DDGI 每个探针的深度缓冲开销(讲义 slide 80)。下一章收拢这些读数的适用边界。
12边界与反例
上一章把两组读数摆在桌上,这一章收拢它们的适用条件。性能表是首先被限定的那一张(讲义 slide 81):表的第一部分给的是逐像素评估的开销,在上一代主机 PS4 上相当贵,到新平台上变得可管理;其中相当大的一块花在层级遍历上,而它与他们某些需求绑在一起,本可以大幅简化,他们从未去研究。讲者紧接着把这张表的用途讲清了:他们其实并不逐像素评估,改用重采样到纹理,所以查询次数低得多;为了让测量足够可靠,他们不得不先把负载放大 64 倍,否则结果直接淹没在噪声里。
整体上这是一个他们满意的方案,唯一还称不上完美的地方是查找开销,在一般情形下偏高(讲义 slide 82);因为要重采样到一份辅助数据结构,他们并不介意,但如果能直接逐像素用会更好,而在收尾阶段他们确实找到了这样一种表示(讲义 slide 83、84)。两年前 SIGGRAPH 上有一篇讲 ReLU fields 的短论文,它做的事可以粗略理解成一张浮点值的纹理、查完做饱和、很像 SDF,里面的值被学出来表示一维函数;它评估极快也擅长表示不连续,看着很贴合权重函数这个需求。可惜它有一个内在特性:重建出的函数的梯度总是垂直于不连续处,于是对典型的二维权重函数表现很差。补救办法是给 ReLU field 再配一个额外的、可学习的场,第二个场允许取任意值、再用 sigmoid 压到 0 到 1 区间,最终输出是两者的乘积,他们把它叫作 product field;ReLU 那部分主要负责硬边,sigmoid 那部分负责平滑区域。
product field 还有一条网络给不了的好处(讲义 slide 85 到 87):每个函数可以用不同的分辨率,复杂的用高分辨率、简单的就分一个小场,而代码完全一样,只是在查询之前对 uv 做一点缩放。除权重函数的表示之外,其余一切保持不变;generator network 从一个变成 4 个,评估它们来找出最优分辨率,误差随场的大小变化、通常在某处变平,就取那个分辨率。讲者也如实交代这个过程还需要改进:从 generator 输出的分析里定出来的分辨率,有时与直接优化得到的不一致。全屏查找的计时也给了出来(讲义 slide 88):层级遍历成了主要成本,函数评估只占一小部分,真要逐像素他们肯定会去改层级。
两版之间他最后还是选了旧的那一版(讲义 slide 89、90):他们目前坚持 MLP 版,因为在他们的场景里,product field 省下的性能并不明显,而换成它有一笔不低的生产成本,于是始终没有硬动力去换。product field 并没有上线。
剩下的边界都是讲者交代的。
边界 1:p81 的逐像素性能表,实际并不这么用。 讲者原话是他们并不真的逐像素评估,改用重采样到纹理;为把测量做准还刻意把负载放大 64 倍。这张表只能读成成本结构,不得写成逐像素实时求值,也不得当作实际开销(讲义 slide 81)。
边界 2:p28 是小测试场景,与 p79 的真实地图不是一回事。 「1 英寸烘焙 1 到 2 小时、训练约 16 小时」绑定那个约 500 体素、平时约 20 秒烘完的小测试场景;p79 是若干张真实地图的预计算时间表。两处口径不同,不得并列成同一场景的前后对比(讲义 slide 28、79)。
边界 3:p80 的两个数分母不同。 「省约 33% 内存」的分母是 NLG 自身的内存用量;「约 1KB/探针」说的是 DDGI 每个探针要存的唯一 2 通道深度缓冲。两个数不能混算成一个「NLG 比 DDGI 省 33%」的说法(讲义 slide 80)。
边界 4:Cornell Box 的两组对比密度不同。 8 个探针那一组里 DDGI 有不连续伪影,9×9×9 那一组里两者看起来几乎一样、NLG 内存更少。引用必须逐张对齐密度,不得把两种密度下的结论并成一句(讲义 slide 70、72)。
边界 5:平台级与运营读数一律属讲者所述。 约 4TB 训练数据、约 18 小时收敛、每秒约 6 万探针对每秒 50 到 60 探针、约 20 万探针场景相交计算不到一分钟、8192 点与 256 点与 32 邻居这些参数,以及 AMD 5995WX 加 RTX 3090 上的 PyTorch 对比,都没有论文、没有受控实验、不可复现,正文凡出场即写明「讲者所述」(讲义 slide 55 到 57、61、63、79)。
边界 6:不与任何商用方案横比。 NLG 深度绑定 CoD 的预计算光照管线,运行时合成要靠表示的线性性(讲者归到 2020 年的 UberBake),查询接口沿用 lightgrid,烘焙跑在光照师本机。p70 到 p73 与 DDGI 的对比是讲者自己在讲义里做的、对象是学术方法。收益与开销都不可外推到别的引擎(讲义 slide 44、70 到 73、79)。
边界 7:它仍会漏。 讲者在缺点页自陈,这套东西归根到底仍是体积表示,比过去锐利得多但仍会漏。他能马虎,是因为要重采样、不会用到太贴近墙的值,甚至可以省掉很多探针的精化、靠 generator network 加快预计算;若要逐像素重建,就需要更精确的表示,也要更多优化时间(讲义 slide 91)。
边界 8:参数来自实验取值,结果是讲者那种特殊情况。 8192 个支撑点、256 个光照采样点、最近 32 个邻居这些数,讲者都说是实验的结果、是质量与成本之间的平衡;p93 的收尾原话是这套东西对他们管用、他们满意,但他们是相当特殊的情况,别人的结果可能不一样(讲义 slide 49、55、56、93)。
本章可核对的结论有两条。逐像素性能表按「其实不用」、重采样到纹理、测量时放大 64 倍的口径引用;p28 的小测试场景与 p79 的真实地图不可混;p80 的百分比以 NLG 自身内存为分母(讲义 slide 28、79、80、81)。Cornell Box 的对比按 8 探针与 9×9×9 两组分别引用,product field 未上线、仍会漏、参数为实验取值(讲义 slide 70 到 73、87 到 91、93)。
13原文出处块
原文出处与引用口径
出处:SIGGRAPH 2024 · Advances in Real-Time Rendering in Games(Part I,2024-07-30,Denver)· 讲者 Michał Iwanicki(Activision Central Technology)· 官方讲义《Neural Light Grid: Modernizing Irradiance Volumes with ML》,98 页,540×720 pt(竖版),来源 PDF 2024-siggraph-advances-neural-lightgrid.pdf(元数据 author=Iwanicki, Michal、title=NEURAL LIGHT GRID)
材料:官方讲义 98 页,其中 84 页带讲者备注(第一人称口播脚本,合计约 3.7 万字符,有文本层);幻灯区为单张整幅位图 1199×674、无文本层,页内标题与数字未经 OCR 复核,须人工过图确认;本篇事实、读数与页码取自讲者备注;页码为 PDF 页序(从 0 起),引用写作「讲义 slide N」
许可:全文检索(正文、讲者备注与页脚)无 © / Copyright / Creative Commons / CC BY / MIT / License 任何声明(0 命中),页脚也无版权行;全篇未给代码仓库或开源链接。故 21 件图按分区非 CC 口径逐件署名(源 slide 号 + 出处 + 「未修改引用」),并在构建记录标注残留风险,不得写成 CC 或可自由转载
引用口径:讲者的单场景实测(1 英寸烘焙 1 到 2 小时、训练约 16 小时,绑定那个约 500 体素的小测试场景)与讲者所述的生产读数(约 4TB、约 18 小时、每秒约 6 万探针对每秒 50 到 60 探针、约 20 万探针场景相交不到一分钟、Threadripper 每秒 60 到 80 个权重函数、8192 / 256 / 32 与 26 万条连接、AMD 5995WX 加 RTX 3090)分开标注;p80 的「省约 33%」以 NLG 自身内存为分母、「约 1KB/探针」是 DDGI 每探针;p81 的逐像素性能表按讲者原话其实并不逐像素评估、实际重采样到纹理并为测准放大 64 倍;p70 到 p73 的 Cornell Box 对比按 8 探针与 9×9×9 两组分别引用;product field 按 p90 的未上线口径引用;全篇不与任何商用方案横比