"Hi. Welcome to the talk. Today we are going to discuss Visibility Buffer rendering and variable rate shading."
- 出发点在眼睛,不在硬件:我们看不到像素、看到的是梯度,而眼睛本身就是一张用这些梯度拼出图像的神经网络(slide 4)。于是优先级排出来了 —— 边缘最强、优先保,细节次要(slide 9);正题是用 Visibility Buffer 把几何率与着色率分离,从而可以用任何采样图案(页内列出 1x/2x/4x/Stochastic),再配一个边缘感知的重建,在采样率与性能之间换(slide 10)。
- 一笔必须先记住的账:像素着色器以 2×2 的 quad 全速运行,quad 没填满时空出的像素仍要跑(跑在屏幕上的是 active lane,即将被丢弃的是 helper lane,结果会被丢掉,slide 16)。于是 大三角形 1 倍、约 10 像素 2 倍、一像素 4 倍(slide 26)。
- 硬件 VRS 只解决了一半:2×2 VRS 下只要三角形碰到 16 个像素里的任何一个就唤起一个 2×2 quad(slide 30),三档账变成 大三角形 0.25 倍、约 10 像素 1 倍、一像素仍 4 倍(slide 35)。讲者由此问出全篇的动机:如果在软件里做 VRS,能不能到处都拿 0.25 倍(slide 36)。
- 三条路径在一像素三角形上的账完全不同:Forward 整趟 4 倍;Deferred 只有 GBuffer 写那趟 4 倍、光照 1 倍;VBuffer 一律 1 倍,而且容易套软件 VRS,代价页内写得很直 —— Downside: Extra overhead(slide 47)。
- VBuffer 的结构只有一句话,要解的问题有四件:「多一趟的 GBuffer」,而实践上复杂得多(slide 48);四件事是 Primitive ID、取插值量、材质求值、VRS 重建(slide 52)。Primitive ID 有六种取法,能当统一路径的只有 SV_PrimitiveID/NonIndexed/Leading Vertex 三种,Mesh Shaders/Software Rasterizer/Geometry Shader 能作后续优化、不能作统一路径(slide 66);选型的标准页内写死为 "Good enough everywhere" 与 No major performance cliffs(slide 54)。
- Leading Vertex 胜出靠的是一份公开实测:三平台读数出自 Sebastian Aaltonen 公开发布的推文(不是讲者本人测得),折算成相对基线的倍数后是 Leading Vertex 1x/1x/1x、SV_PrimitiveID 1.5x/1.8x/1x、Non-Indexed 2.0x/2.6x/1.6x(slide 68–71)。它的原理只有一行(flat 插值只取第一个顶点,slide 76),难点在让每个三角形都摊到一个空闲起点:两步排布(slide 77/91),簇固定 64 三角形/64 顶点,最后落到 每三角形两个 6 位索引、32 位索引下 5.5 字节/三角形(slide 95)。
- 后三件事都带着讲者自己的让步:导数甩不掉 quad,得自己算并改用
SampleGrad()(slide 103–104),那段重心坐标导数来自 Schied 与 Dachsbacher(HPG 2015)、代码由 James McLaren 与 Stephen Hill 提供,且讲者博客那版有错、是两人纠正的(slide 105);重建靠实例 ID/深度/几何法线做边缘检测(slide 117),搜索放弃洪泛填充、改用一条硬编码路径(slide 122–123)与位掩码布尔表达式(slide 129),页内自评 TODO: Optimize(slide 130);性能上完全可变速率在光照变化时有鬼影、需要不断调参,可靠的是 1x/2x/4x 固定图案(slide 136)。
1这是个什么问题:把几何率与着色率拆开
这是个什么问题:把几何率与着色率拆开
这一篇要处理的问题,题目页就摆明了:用可见性缓冲(visibility buffer / VBuffer)配合可变速率着色(variable rate shading / VRS),把几何率(geometry rate)与着色率(shading rate)拆成两件独立的事(讲义 slide 0)。在进正题之前,先把讲者的来路交代清楚(讲义 slide 1、2)。页内署名是 John Hable,职务一栏写着 Founder, Visible Threshold, LLC;另有一栏 Previous,列着他此前待过的地方:Electronic Arts、Naughty Dog、Epic、Unity,以及一串合同与自营项目。按同页讲稿,他大部分时间在做主机游戏与引擎,也做过摄影测量棚与电影预演用的虚拟相机,去年十一月之前他还在 Unity 的 Natasha 图形组。这一页只作定位。
第二页交代公司方向(讲义 slide 2)。页内把 Visible Threshold 要做的事写成一行 USD for the Web,旁边并排两条线:一条说 USD 生态正在快速成熟,页内点了 MaterialX、MDL 与 OpenPBR 1.0 三个名字;另一条说 WebGPU 已正式发布并开始被主流浏览器采纳,而 WebGL 被写成可接受的回退。讲稿把这件事补完整,说目标是把一套 USD 场景变成独立的网页体验、要解决的是其中的工作流问题。这段同样只作定位,正文不从这里引出技术结论。
真正切入的是 slide 3 那行 What is this and why do this?,紧接的一页给出整场的出发点(讲义 slide 4)。页内只有三行,Key Idea 下面写着:我们并不「看见」像素,我们「看见」的是梯度;眼睛本身就是一个神经网络。讲稿把最后一句说得更直:眼睛是一个神经网络,它拿这些梯度拼出一张幻觉出来的图。讲者不从硬件特性讲起,从眼睛怎么接收信息讲起。
由此往下,讲义用一次逐级放大把「梯度有轻重」这件事讲实(讲义 slide 5 至 8)。这四页是同一张图的连续放大,用绿、黄、红三种颜色的框标出三条性质不同的边:绿框是一条几何边界,页内注明非常重要、必须保住;黄框是一处纹理细节,重要但次一等;红框那两个像素的差异不可察觉。讲稿给绿框补了物理缘由:一个像素被太阳直接照到,紧挨着的一个只收到间接反弹的光,画面上出现一条陡的亮度边。这四页只作文字描述,不另取图。
把这套观察收成两句优先级的是 slide 9(讲义 slide 9)。页内在 Key Idea 下并列两行:最强的边出现在边界处,排在第一位;细节也重要,排在第二位。讲稿说明这条优先级要拿来做什么:用可变速率着色保住场景里的这些强边,同时把几何边内部的采样率降下来,就能用更少的渲染时间换到感知上相近的画面。
落到具体做法的一页是 slide 10(讲义 slide 10)。页内先写 Variable Rate,再写一行 Visibility Buffer 能把几何率与着色率解耦,后面跟着四个可选的采样图案:1x、2x、4x 与随机采样(stochastic)。第三行的 Reconstruction 后面写着一句 Always have "good" edges,意思是重建(reconstruction)之后画面上始终有「好的」边。页末一行是这一章的代价:上采样会牺牲边,取舍落在细节与性能之间。讲稿把这几行合成一句:把两个速率彻底分开,于是可以用任何喜欢的采样图案,再配一个边缘感知的重建,就能在采样率与性能之间换,同时保住画面里的主导边缘。
本章可核对的结论有三条。这个题目要解决的是把几何率与着色率拆开(讲义 slide 0),做法是 VBuffer 加 VRS。出发点是眼睛:我们看不到像素、看到的是梯度(讲义 slide 4)。由此得到两条优先级,边界上最强的边排第一、细节排第二,采样图案从 1x、2x、4x 到随机采样都可以选,取舍写在页末一行:上采样会牺牲边(讲义 slide 9、10)。下一章去看「一个三角形值多少个像素」这笔账。
2四边形利用率:helper lane 的那笔账
上一章把出发点与两条优先级摆完,这一章把这笔账算清楚(讲义 slide 13)。讲义在这一页只给了一个分区横幅 QUAD UTILIZATION,讲稿说明它的必要性:在进到 VBuffer 与 VRS 之前,得先把四边形利用率(quad utilization)这件事弄扎实。紧接着的一页把问题摆成两问(讲义 slide 14):三角形光栅化在 GPU 上到底怎么跑,答案是四边形利用率。
机制先讲在 slide 15 与 16(讲义 slide 15、16)。页内先说像素着色器是分组进 quad(四边形,2×2 像素组)的,而且是永远的四人一组;另有一行说得更细:你写的 MainPS 只处理一个像素,但底下永远是一次跑四个。页内还提到 SM 6.0 把这套机制暴露出来了,举的例子是 QuadReadAcrossX 一类函数。第二页讲不满的 quad 怎么处理:如果一个 quad 没被填满,空出来的位置由 helper lane(辅助 lane)填上,而 helper lane 上的像素着色器结果会被丢掉。同页讲稿把这组名字给全:真正要出现在屏幕上的像素叫活跃 lane(active lane),即将被丢弃的那些像素着色器调用叫辅助 lane(helper lane)。
从一个 quad 被多个三角形覆盖的例子往下,账就出来了(讲义 slide 17 至 19):讲义先设问,一个 quad 被多个三角形覆盖会怎样(讲义 slide 17);下一页给答案,每个三角形各拿一个自己的 quad(讲义 slide 18)。讲稿把这个例子拆得更细:绿、蓝、红三个三角形各拿一个 quad,活跃 lane 依次是一个、两个、一个,剩下的位置都由 helper lane 填满。第三页把这个结果接上「低效」这个问题,页内先自问自答一句 Yes!,再把它归到与四边形重复着色(quad overdraw)相似,并给出这一例的读数 3x(讲义 slide 19)。讲稿说在他的理解里,quad 就当作重复着色来想,因为 GPU 不得不把像素着色器跑好几遍。
尺寸决定代价,两页各管一头(讲义 slide 20、21):大三角形那页说多余的 helper lane 成本低,讲稿解释成活跃 lane 多、helper lane 少,成本就边缘化;一像素三角形那页说 helper lane 带来 4x 代价,讲稿把缘由点透,一个只覆盖一个像素的三角形仍需要一个完整的 2×2 quad。
中间那段是典型尺寸,讲义连着用了四页,给的都是约 10 像素三角形的平均读数(讲义 slide 22 至 25):理想情形 1.2x,对应 10 个活跃 lane、2 个 helper lane、83% 利用率;4x4 形状 1.9x,对应 9 个 helper lane、53% 利用率;8x2 的 L 形 2.1x,对应 11 个 helper lane、48% 利用率;最坏的又细又斜 3.1x,对应 21 个 helper lane、32% 利用率(同页讲稿口头说的是 3.2x)。代价随形状与屏幕栅格的对齐变化,四页读数从 1.2x 摆到 3.1x。
把这一整段收成一张总账的是 slide 26(讲义 slide 26)。页内在 Triangle Size/Quad Overdraw 下面并列三行:大三角形是 1x 代价;约 10 像素的中等三角形是 2x 代价;一像素的微小三角形是 4x 代价。讲稿说这一页是全场最该记住的东西,并交代了他算这笔账的方式:逐个图案去测当然可以,但他用一种简化的想法处理,大三角形不必操心头,一像素三角形付 4 倍,典型游戏三角形大约 10 像素、付 2 倍。他还提醒,很多人以为四边形重复着色只有做超密网格时才要紧,可 2x 仍然很大,10 像素的三角形今天就在为这笔开销付钱。讲者对自己的简化并不掩饰,明说这是把逐图案测量简化掉之后的口径。
本章可核对的结论有三条。像素着色器永远以 2×2 的 quad 四人一组运行,写 MainPS 时只处理一个像素,底下跑的永远是四个(讲义 slide 15)。quad 没填满时由 helper lane 补上,helper lane 上的结果被丢弃,要上屏幕的那些才叫活跃 lane(讲义 slide 16)。由此得到三档账:大三角形 1x、约 10 像素三角形 2x、一像素三角形 4x,中间那一档在页内四个例子里依次读到 1.2x、1.9x、2.1x 与 3.1x(讲义 slide 22 至 26)。讲者明说这是他简化之后的口径。下一章把这套账拿到硬件 VRS 上再算一遍。
3硬件 VRS 撞上四边形利用率
上一章把三档账定下来:大三角形 1x、约 10 像素的中等三角形 2x、一像素的微小三角形 4x(讲义 slide 26)。这一章把这套账放到硬件可变速率着色上再看一遍(讲义 slide 27)。页内先设一个问题,硬件 VRS 会不会受四边形重复着色影响。同页讲稿交代了动机:缓减像素着色器成本有一条现成的路是硬件 VRS,就得看它对四边形重复着色做了什么。
先把规则栅格化重述一遍(讲义 slide 28、29)。页内两行写的是:像素着色器调用被切成 2×2 的 quad;只要三角形碰到任何一个像素,就要生成一个 2×2 quad。讲稿把这句讲开,屏幕被切成 2×2 的 quad,三角形碰到其中 4 个像素里的任何一个,就得唤起一个 2×2 quad。这两页是同一页内容的连续两格。
加上 VRS 之后是 slide 30 与 31(讲义 slide 30、31)。页内在 Regular Rasterization 下接着写:有了 VRS,每个 2×2 quad 里装着 2×2 块像素,也就是 4×4;同样地,只要三角形碰到任何一个像素,就要生成一个 2×2 quad。讲稿说得更顺,同一套概念,只是 quad 变大;以 2×2 VRS 为例,三角形碰到这 16 个像素里的任何一个,就要唤起一个 2×2 quad。
三档账在硬件 VRS 下各算一次,讲义连着用三页(讲义 slide 32 至 34)。大三角形那页写着大三角形配 VRS 很好、多余 lane 仍然边缘化,读数是每像素 0.25x 次像素着色器调用,页内把它写成「想要 0.25x,拿到的差不多就是 0.25x」;讲稿留了一句让步,即便在这种情形,helper lane 也在开始累积。一像素三角形那页写着每个三角形至少催生一个 quad,读数是每像素 4x 次像素着色器调用,页内直写「想要 0.25x,拿到的是 4x,与不用 VRS 时没有差别」;讲稿解释这个结果,一个像素的三角形走到哪都要唤起一个完整的 2×2 quad。中等三角形那页写着每个三角形 10 个像素、大约每像素 1 次像素着色器调用,页内同样直写「想要 0.25x,拿到的是 1x」。三页合起来说明,硬件 VRS 只在三角形够大时才帮上忙。
把三档并排放在一张表上的是 slide 35(讲义 slide 35)。页内先给三行结论:大三角形上有 4x 的好处,中等三角形上有 2x 的好处,微小三角形上没有变化。表头并排两列 Regular 与 2x2 VRS,三行依次填着:大三角形 1x 与 .25x;约 10 像素三角形 2x 与 1x;一像素三角形 4x 与 4x。讲稿的读法与表一致:大三角形在硬件 VRS 下很好,微小三角形仍然付 4x 的罚,10 像素三角形落在 1x,比不用 VRS 好,但还没有好到他们希望的程度。这张表里最要紧的是最后一行,两列相同,这一格空白就是全篇的动机。
动机在 slide 36 上被写成几个连续的问句(讲义 slide 36)。页内在 What if? 下面连着问:把 VRS 放到软件里做会怎样,能不能到处都拿到 0.25x 的好处,连微小三角形也能拿到?表头在 Regular 与 2x2 VRS 之外又添了一列 Software VRS,三行依次填着:大三角形 1x、.25x 与 .25x;约 10 像素三角形 2x、1x 与 .25x;一像素三角形 4x、4x 与 .25x。最后一列三行全是 .25x,这一列是整场演讲想要达到的样子。讲稿把话说得很直,这就是整场演讲的全部要点,而且它确实做得到,只是要为此付一些额外开销。这句让步要照写,软件 VRS 的那份好处是有代价的。
本章可核对的结论有三条。规则栅格化把屏幕切成 2×2 的 quad,三角形碰到任何一个像素就要唤起一个 2×2 quad;加上 VRS 之后同一套规则只是 quad 变大,在 2×2 VRS 下碰到 16 个像素里的任何一个就要唤起一个 quad(讲义 slide 28、30)。三档账在硬件 VRS 下是:大三角形从 1x 降到 .25x,约 10 像素三角形从 2x 降到 1x,一像素三角形两列都是 4x、没有变化(讲义 slide 35)。讲者的动机写在 slide 36,若把 VRS 放到软件里做,三档都能拿到 .25x,这就是整场演讲的要点,代价是要付一些额外开销(讲义 slide 36)。这笔额外开销到底花在哪里,要从三条渲染路径各自的账看起。
4三条渲染路径的代价:forward/deferred/VBuffer
上一章把动机收到软件 VRS:到处都拿 0.25 倍,代价是额外开销(讲义 slide 36)。要看清这笔额外开销值不值,得先把三条渲染路径各自的账摆出来,这一章做的就是这个(讲义 slide 37 至 47)。讲义先用一个分区横幅 VBUFFER DETAILS 把这一组隔开(讲义 slide 37)。三条路径在讲义里是按同一套骨架讲的:每条路都问「材质求值与光照各落在哪一趟」,差别只在这一趟怎么拆。三条路最后都压到一像素三角形上来算,因为那是前两章三档账里最重的一档(讲义 slide 47)。
第一条是前向着色(forward shading)(讲义 slide 38 至 40)。页内只有一行,材质求值与光照在同一趟里做完。讲稿把这一趟展开成一段话:像素着色器从顶点拿到插值量,材质求值函数拿这些插值量算出 BRDF 数据,光照求值函数再拿材质数据算出结果光。这一组是三张内容相同的连拍页,取第一张作代表。
第二条是延迟渲染(deferred rendering)(讲义 slide 41 至 43)。页内两行,第一趟写 GBuffer,第二趟做光照。讲稿补了一层:第一趟像素着色器把材质数据写进一个缓冲,第二趟(可以是像素着色器,也可以是计算着色器)取回材质数据、求出光照、把结果写出去。这也是三张内容相同的连拍页,取首张作代表。
第三条是 VBuffer 渲染(讲义 slide 44 至 46)。页内三行,第一趟写三角形 ID(triangle ID),第二趟写 GBuffer,第三趟求光照。讲稿把它讲成「再拆一次」:第一趟把三角形 ID 写出去,就这一件事;然后材质那一趟要去取三角形 ID、取插值量、做材质求值并写 GBuffer;最后光照那一趟与前面没有区别。这一组同样是三张内容相同的连拍页,取首张作代表。
把三条并排看,趟数依次是一、二、三;讲义在这一组页里没有给任何耗时数字,只交代结构(讲义 slide 38 至 46)。
把三条并读、落到一像素三角形上的收口页是 slide 47(讲义 slide 47)。页内标题是 Cost for 1 Pixel Triangles,下面三个小标题并排:Forward 一栏写整趟都是 4x 代价;Deferred 一栏写 GBuffer 写那一趟是 4x 代价、光照是 1x 代价;VBuffer 一栏写一切都 1x 代价、软件 VRS 好套上去,另单列一行 Downside: Extra overhead。讲稿把三条并读一遍:前向那条路上,材质加光照合在一起的那个像素着色器要在 4x 重复着色下跑;延迟渲染里材质那趟是 4x,可光照只跑 1x;VBuffer 下材质与光照每像素只跑一次,代价是多出来的复杂度。三条路算的是同一件事,拆法不同,账也就不同。
本章可核对的结论有三条。三条路径的趟数并不相同:前向着色把材质求值与光照放进同一趟;延迟渲染拆成写 GBuffer 与光照两趟;VBuffer 再拆一次,第一趟只写三角形 ID、第二趟取三角形 ID 与插值量并写 GBuffer、第三趟求光照(讲义 slide 38、41、44)。落到一像素三角形上,页内把代价写成:Forward 整趟 4x,Deferred 写 GBuffer 那趟 4x、光照 1x,VBuffer 一切都 1x、软件 VRS 好套上去,另有一条代价是多出来的开销(讲义 slide 47)。VBuffer 那一栏为什么「理论上简单、实践上复杂」,下一章从它的结构讲起。
5VBuffer 的结构,和那句「理论上简单」
上一章把三条路径的账并排摆开,一像素三角形上前向着色(forward shading)整趟是 4 倍代价、延迟渲染(deferred rendering)只有写 GBuffer 那一趟是 4 倍,而 VBuffer 每一趟都 1 倍,代价栏里另写着一条 Downside: Extra overhead(讲义 slide 47)。用 1 倍换来的这笔额外开销具体花在哪里,从这一章起逐项交代。这一章先把这套结构说清,再把要解的账目一次列出来,那四条也正好是后面几章的章节顺序。
进入 VBUFFER DETAILS 这一段后的第一页只有两行字(讲义 slide 48)。上一行是 Theoretically simple,下面缩进一行 Just GBuffer with an additional pass.;紧接着另起一行,写着 Vastly more complicated in practice.。页内把这两句上下对着排,上半句给结构上的类比,下半句给落地时的实情。同页讲稿说得更松一点,他说看 VBuffer 渲染这份大纲就知道里面还藏着几个麻烦的细节(讲义 slide 48)。
「多一趟」这三个字要按上一章那条路径读。VBuffer 的第一趟只写图元编号(primitive ID),也就是三角形编号(triangle ID);第二趟把三角形编号与插值量取回来,做材质求值并写 GBuffer;第三趟做光照(讲义 slide 44)。页内说它是 GBuffer 多加一趟,指的是它在 GBuffer 之前多出那趟只写编号的趟,光照那一趟与延迟渲染那边结构相同(讲义 slide 44、47)。这一页没有给任何耗时或带宽读数,读的时候把多出来的那一趟与第 4 章那张三行表对上即可,这一趟在页内的全部内容就是写编号。
要解的账目摊在随后四页上,页内逐条累加,前面几条继续留在版面上(讲义 slide 49、50、51、52)。第一条是 Primitive ID,页内写着 Easy if we don't care how slow it is.,旁边补一行 Tricky to get without performance penalty.(讲义 slide 49)。这两行说的是难处落在性能上;同页讲稿只问了一句,写 VBuffer 的那一趟里那个三角形编号究竟怎么拿(讲义 slide 49)。页内把这两句并排摆着,好不好做与够不够快被拆成两件事。
第二条是取插值量(Fetch Interpolators),页内写着 Requires Mesh Pool 与 And a few details for performance(讲义 slide 50)。同页讲稿把这条的分量讲明了,取插值量这件事硬件不能再替我们做了(讲义 slide 50);顺着它往下会走到网格池(mesh pool),第 9 章专讲。第三条是材质求值(Material Evaluation),页内只剩一行 Key issue is derivatives(讲义 slide 51),讲稿补上口径,材质求值本身没有变,要自己想办法的是 mipmap 需要的那份导数(讲义 slide 51)。页内给这一条只留了半句提示,落到实现上就是要把顶点与索引放到一个可以按编号寻址的地方,第 9 章会给出这件事的形状。
第四条是 VRS 重建(VRS Reconstruction),页内另带一问 For software VRS, how do we choose samples and reconstruct later?(讲义 slide 52)。同页讲稿把收尾说成一句贯穿整套流程的话,最后要把这些像素分开、排序、再拼回一张最终图像(讲义 slide 52)。这一问的答案在第 11 章。四件事在版面上是层层加上去的,读的时候能看出它们不在一个层面上,前两条管数据从哪来,第三条管求值,第四条管最终那张图怎么拼。
本章可核对的结论有三条。VBuffer 在页内被写成 Theoretically simple 与 Just GBuffer with an additional pass.,同时并排写着 Vastly more complicated in practice.(讲义 slide 48)。要解的四件事按页内顺序是 Primitive ID、Fetch Interpolators、Material Evaluation、VRS Reconstruction,其中取插值量需要网格池、材质求值的要点是导数、重建那一页另带一句关于软件 VRS 该怎么选样本与怎么还原的自问(讲义 slide 49、50、51、52)。四条里页内只给性质与问句,可以拿来定位的具体页是讲义 slide 48 的那两行与讲义 slide 52 的那一问。下一章从第一条开始。
6Primitive ID 有六种取法,能当统一路径的只有三种
上一章把四件事列出来,排在第一的是 Primitive ID(讲义 slide 49)。这一章把这条展开,看那颗写进渲染纹理的三角形编号有几种拿法,以及其中哪些能当统一路径。
这段的第一张内容页先把目标说清(讲义 slide 54)。页内说要渲染出这样一张图,每个像素里存着三角形编号、放进一张渲染纹理,紧接着自问 How to do it?。选型的原则跟在后面,页内写成 One solution for all platforms,下面并排三句:Good enough everywhere、Can add optimized solutions later、No major performance cliffs(讲义 slide 54)。讲稿把这条标准讲得更直,判据落在「哪里都不会掉下去」,他们想要的是一个到处都能用、没有大的性能悬崖的方案,页内给出的目标也只是「够好」,它把「以后再加优化方案」也写进了原则里,留出的是后续改进的余地(讲义 slide 54)。
候选挂在下一页(讲义 slide 55)。页内按顺序排出一列:SV_PrimitiveID、NonIndexed、Leading Vertex、Mesh Shaders、Software Rasterizer、Geometry Shader;同页讲稿只有一句,选项很多。随后几页里这列候选一直留在版面左侧,正在讲的那一条被放大展开(讲义 slide 56 至 65)。
SV_PrimitiveID 或 gl_PrimitiveID、实现是 Trivial to Implement,并列出平台支持与性能惩罚两条限制。②读法(怎么读):把它读成候选清单的入口页,后面每页换一条展开而清单不动;讲到最后一条之后,下一页把候选收成两栏。
来源:SIGGRAPH 2024 · Advances in Real-Time Rendering in Games course · Variable Rate Shading with Visibility Buffer Rendering · 讲者 John Hable(Visible Threshold, LLC)· 未修改引用
第一条是 SV_PrimitiveID(讲义 slide 56)。页内给的用法是 SV_PrimitiveID 与 gl_PrimitiveID 两个语义,评价是 Trivial to Implement;限制栏里写着 Not supported on some platforms 与 Performance penalty on some platforms,中间夹着一行 Check your NDAs。讲稿把两面都说了,它极容易实现,字面上就是一行代码,可它有几个平台不支持,也带着一些性能惩罚(讲义 slide 56)。要点收成一句,一行代码,却不是到处都能用。
第二条是 NonIndexed(非索引)(讲义 slide 57 至 59)。页内三行写着 Render 3 Vertices、Primitive ID as color、3x VS invocations per triangle。讲稿用一个立方体把账讲开,第一个三角形要渲染三个顶点,想让整块三角形拿到同一个粉色编号,就得给三个顶点都写粉色;到下一个三角形再渲染三个顶点,颜色换成另一个。做法简单,代价是丢掉顶点复用的好处,每个三角形需要 3 次互不相同的顶点着色器调用。
第三条是 Leading Vertex(前导顶点)(讲义 slide 60 至 62)。页内先写 Use nointerpolate/flat interpolation、First vertex is "Provoking"、Other two are ignored,随后几页补上 Allows vertex reuse、1x VS invocations per triangle、Works on all platforms。讲稿把这条叫一个技巧,用 nointerpolate 或 flat 着色时,插值器只取一个顶点的颜色,另外两个顶点的颜色被忽略,被取中的那个叫 provoking vertex(触发顶点),每个三角形因此只付 1 次顶点着色器调用。讲稿在这里留了余地,说这离「每个三角形半个顶点」的理论理想还差一截,可比 NonIndexed 的 3 次要好几倍(讲义 slide 60 至 62)。
后三条一律先给优点再给限制(讲义 slide 63 至 65)。Mesh Shaders(网格着色器)页内优点为 Peak vertex performance 与 Best memory,限制是 Not supported on all platforms,讲稿说如果最低配置支持它就选它,可它离全面支持还很远。Software Rasterizer(软件光栅器)优点为 Fastest option on some hardware,限制为 64bit atomics not supported by all platforms 与 Requires a platform-independent fallback,讲稿另补它需要 64 位原子操作、通常还要为大三角形准备一条回退、移动端会有耗电问题。Geometry Shader(几何着色器)页内只有一句 Very inconsistent performance on different hardware,讲稿说它能用,可各厂商之间的性能差得极不一致。
候选的归属在讲义 slide 66 收成两栏,页内那行注把两件事分开:These are viable as an optimization, but are not viable as a unified path for all platforms.。讲稿说得更细,完整方案只剩左边那三条,后面的网格着色器与软件光栅器是以后做优化的好候选,第一版实现里用不上(讲义 slide 66)。这条让步要照写,能作优化与能作统一路径是两件事。
本章可核对的结论有三条。选型原则写在页内 One solution for all platforms 下面,目标以「Good enough everywhere」为准、以后可以再加优化方案、不要有大的性能悬崖(讲义 slide 54)。候选在页内列成一列,可用的三条是 SV_PrimitiveID、NonIndexed 与 Leading Vertex,另外三条是 Mesh Shaders、Software Rasterizer 与 Geometry Shader,页内注明后三条能作优化、不能作统一路径(讲义 slide 55、66)。三条里第一条一行代码但支持面窄,第二条要付 3 次顶点着色器调用,第三条能把调用压到 1 次并且到处能用。下一章去看那份公开的三平台实测。
7三张卡上的实测:Leading Vertex 胜出
上一章把可用的三条取法收在左边一栏(讲义 slide 66)。要在三条里选一条实现,就得有性能数据,这一章交代这份数据从哪来、折算之后谁领先。
难点先摆出来(讲义 slide 67)。页内写着 Not practical to test all platforms.,接着两行并排说明,一句说今天的 GPU 与十年前的差别很大,紧接一句用感叹号强调十年前的 GPU 仍然相关,下面是 Sebastian Aaltonen to the rescue. 与 Numbers for NVIDIA, AMD, and Intel。页内把这条取舍摆得很清楚,一句说把所有平台都测一遍不现实,另一句说十年前的 GPU 仍然相关,这份调查因此还有用。讲稿把这段讲成一次援手,好在 Sebastian Aaltonen 几年前做过一次系统的调查,并把结果发在推特上,测的是一张 NVIDIA、一张 AMD、一张 Intel 机器(讲义 slide 67)。这一章的读数全部来自这份公开发布的调查,不是讲者本人测得的;页内在这一页下方给出推文链址,三台机器的数据归 Aaltonen。三台机器用的是同一批测法,本节的所有对照都在这一份实测内部进行。
第一台是 NVIDIA(讲义 slide 68)。页内写着 Leading Vertex: 0.883、SV_PrimitiveID: 1.367、Non-Indexed: 1.750,并注明 Baseline missing from tweet.。讲稿的读法是,在这台卡上 Leading Vertex 最快,SV_PrimitiveID 大约慢 50%,NonIndexed 大约慢 100%(讲义 slide 68)。页内对基线缺失另有两行交代,一行说它与后来一版 NSIGHT 抓取里的 Leading Vertex 相符,另一行是一句自嘲,Not easy to fit nuanced graphics experiments into 140 characters.;这两行把数据的来路与局限一并交代了。这一页右侧那张汇总表已经出现,只填了 NVIDIA 一栏。
第二台是 AMD RDNA1(讲义 slide 69)。页内写着 Baseline: 2.06、Leading Vertex: 2.16、SV_PrimitiveID: 3.78、Non-Indexed: 5.45,并附一句 Slight cost for leading vertex。讲稿说这台与 NVIDIA 那台情形相近,Leading Vertex 只多一点点,SV_PrimitiveID 与 NonIndexed 贵得多(讲义 slide 69)。页内那句 Slight cost 就是从这条读数来的,这一台相对基线高出的幅度很小,右侧的表在这一页补上 AMD 一栏;这句注把这一台上唯一一处代价点了出来,另外两条取法与它的差距则大得多。
第三台是 Intel iGPU(Gen12)(讲义 slide 70)。页内写着 Baseline: 12.63、Leading Vertex: 12.83、SV_PrimitiveID: 12.87、Non-Indexed: 20.17。讲稿说这台有意思,Intel 的 SV_PrimitiveID 实现非常快、几乎免费,Leading Vertex 也一样,NonIndexed 则要付明显的代价(讲义 slide 70)。这一台与另外两台的分歧落在 SV_PrimitiveID 的位置上,它在 NVIDIA 与 AMD 上都贵出一截,在 Intel 上却与 Leading Vertex 持平;页内没有为这个分歧给解释,只把三台的读数摆出来。
三台读数被折算成相对各自基线的倍数,收在段末一张总表上(讲义 slide 71)。页内写 Leading Vertex 在三栏都是 1x,SV_PrimitiveID 是三栏 1.5x、1.8x、1x,NonIndexed 是三栏 2.0x、2.6x、1.6x,并注明这是与基线相比的粗略换算、并且做过四舍五入;这一页只给这三行换算与一句结论,没有再把原始读数逐项列出;三列并排也是本节唯一一处能把三台机器横向对齐看的地方。讲者的结论只有一句,Leading Vertex is the clear winner.;讲稿把它展开,折算以后 SV_PrimitiveID 与 NonIndexed 各有各的性能惩罚,而 Leading Vertex 相对基线最多只多一点点(讲义 slide 71)。
本章可核对的结论有三条。三台机器的读数来自 Sebastian Aaltonen 公开发布的推文,测的是一张 NVIDIA、一张 AMD、一张 Intel,不是讲者本人测得(讲义 slide 67 至 71)。原始读数是 NVIDIA 上 0.883、1.367、1.750,页内注明基线在推文里缺失、只靠后来一版 NSIGHT 抓取与 Leading Vertex 相符来补;AMD RDNA1 上基线 2.06、Leading Vertex 2.16、SV_PrimitiveID 3.78、NonIndexed 5.45,页内附一句 Slight cost for leading vertex;Intel iGPU(Gen12)上基线 12.63、Leading Vertex 12.83、SV_PrimitiveID 12.87、NonIndexed 20.17(讲义 slide 68、69、70)。折算后页内三栏是 Leading Vertex 1x、1x、1x,SV_PrimitiveID 1.5x、1.8x、1x,NonIndexed 2.0x、2.6x、1.6x,页内注明这份换算是粗略的并做过舍入,结论是 Leading Vertex 明显胜出(讲义 slide 71)。三页各留了一条推文链址,原始数据可以回去对照。下一章看它靠什么把代价压住。
8Leading Vertex:把「起点」当成一个可以排布的问题
上一章的三台读数把选择收到一条上,Leading Vertex 在三份折算表里都贴着 1x(讲义 slide 71)。这一章看它凭什么做得到,以及为了让每个三角形都摊到一个「起点」要付什么代价。
原理只有一行(讲义 slide 76)。页内写着 Use flat/nointerpolate mode. 与 One color is used for entire triangle.,再把这个顶点点名为 Called the "Provoking Vertex",并标出位置:First vertex on most platforms. 与 Last vertex on OpenGL.。讲稿补一句机制,只要索引按这个样子排好,图元编号就是进来的顶点编号,中间做一次间接寻址把进来的索引换成真正的顶点缓冲索引(讲义 slide 76);flat 或 nointerpolate 只取一个顶点的颜色,整块三角形共用一个编号。
问题因此变成一个排布问题,怎样排索引才能让每个三角形的第一个顶点正好是它编号所对应的顶点。讲义先给一个入口例子(讲义 slide 72 至 75),页内四页逐格搭起来,从 Here are some triangles. 一路问到 Notice a pattern in the first column?。讲稿在示例结束时给出结论,按这个顺序排好之后,图元编号正好等于前导顶点的编号(讲义 slide 75)。
排布在页内拆成两步(讲义 slide 77):Step 1) Initial Tris 与 Step 2) Rotate Vertices。讲稿说第一步是快的贪心尝试,尽量先给三角形定下 provoking vertex,第二步做旋转、把第一步剩下的补上(讲义 slide 77)。第一步先数每个顶点接了几块三角形,页内把它叫 valence(价数):First, count the number of tris touching each vertex (valence).(讲义 slide 78);挑起点用一条直觉规则,页内写 Intuition: We should use the vertex with lowest valence.,讲稿补充说价数低的顶点更可能腾出来给别的三角形用,这条规则也会倾向于先挑边界(讲义 slide 79)。取舍页内写得像伪代码:If vertex is available? 就 Take it,If no vertex is available? 就 Try again in next pass(讲义 slide 80)。第二步在页内用一整段示例演进(讲义 slide 81 至 90),一路演示到三个顶点都被占、再沿邻居找空闲顶点把起点旋进来。算法收成四行(讲义 slide 91):Find valence of each vertex.、For each tri: Choose available vertex with lowest valence.、For each tri: Depth first search for free vertex.、Rotate forward vertex.。讲稿在这里补了一条实用的观察,多数情况下第二步不用跑,第一步就已经给每个三角形找到了合法的 provoking vertex(讲义 slide 91)。
规模与失败面写在同一页(讲义 slide 92):Clusters are 64 tris, 64 verts.;如果三角形比顶点多,比如 64 个三角形配 50 个顶点,解法是先给这 50 个顶点找起点、再挑任意 14 个复制进来填满;失败面是对每个不是 provoking vertex 的顶点,都要从簇(cluster)里丢掉一个三角形。最常见的失败另起一页(讲义 slide 93):Most common failure: 与 Triangle with multiple 1 valence verts.,页内再补 With 64 tris, all verts must map to one unique triangle. 与 Free vertex can not rotate to a triangle that needs it.;讲稿把它讲成一个三角形只靠一个索引松散地挂在簇上,绿色顶点旋不进来,簇里总有一块三角形安排不下(讲义 slide 92、93)。
排布要落到磁盘上的数据(讲义 slide 94)。簇化用(讲者 fork 过的)MeshOptimizer,页内给出出处 https://github.com/zeux/meshoptimizer,并注明它也用来做 LOD;基础算法是把三角形一次一个地加进缓冲,超过限制(64 tris/64 verts)就吐出一个簇,他的改动是吐簇之前按 provoking vertex 重排、排不下的留进下一个簇。讲稿说他也用它做 LOD,因为快得离谱,通常所有三角形都装得下。打包后的账在下一页(讲义 slide 95):每簇 64 个重排索引、每三角形只存两个索引、第一个隐含、每个索引 6 位;32 位索引下是每三角形 5.5 字节、对照未压缩的 12 字节(3 个 U32),16 位索引下是 3.5 字节、对照 6 字节(3 个 U16)。讲稿说这份结构收得很紧,每簇的索引里很可能有重复项(讲义 slide 94、95)。
一处工程让步收在段末(讲义 slide 96)。页内写 Some platforms might need multiple rendering paths.,例子是发行 PC 游戏时可能同时准备 Mesh Shaders、Leading Vertex、Fallback to Regular 几条路径;接着自问序列化怎么办,并写明 Definitely don't want to store 3 versions on disk.。解法是磁盘上只存压缩表示、用它喂 mesh shader、载入时转换成 Leading Vertex 或普通索引表。讲稿说新 GPU 走 mesh shader、老 GPU 退回 leading vertex 甚至普通 GBuffer,盘上最不想存的就是三份网格。
本章可核对的结论有三条。机制上 flat 或 nointerpolate 只取一个顶点的颜色,整块三角形共用一个编号,被取中的顶点叫 provoking vertex,它在多数平台上是第一个顶点、在 OpenGL 上是最后一个(讲义 slide 76)。排布分两步,先数价数、再逐三角形挑价数最低的可用顶点,第二步对剩下的做深度优先搜索并旋转前导顶点,页内注明多数情况下第二步不用跑(讲义 slide 77、91)。打包后每簇 64 个重排索引、每三角形只存两个索引、每个索引 6 位,32 位索引下是每三角形 5.5 字节、对照 12 字节,16 位索引下是 3.5 字节、对照 6 字节(讲义 slide 95)。下一章转到第二条账目,看取插值量为什么需要一个巨大的网格池。
9Mesh Pool 与顶点动画
上一章把簇数据压到磁盘上,用一份压缩表示同时喂 mesh shader 与 Leading Vertex,并且定下了不在磁盘上存三份的规矩(讲义 slide 96)。这一章接着处理一个更基础的问题:给定一个三角形 ID,怎么把它的三个顶点取回来。
讲义 slide 97 是只有分区横幅 MESH POOL 的隔页,页内文字只有这两个词,讲稿在那里说接下来要把网格这件事谈一谈(讲义 slide 97 页内、讲稿)。下一页把问题说清:给定一个三角形 ID,需要取回那三个顶点,做法是把全部几何存进一个大缓冲(讲义 slide 98 页内)。讲稿把这句补成更直的话,要的是把所有网格并进一个巨大的缓冲,散成一堆各自独立的小缓冲就不行(讲义 slide 98 讲稿)。
大池化本身还带一份顺带的好处。页内说它在移动端也有收益,能少几次 API call(API 调用),并给了一句评价 Hard to think of a good reason not to do it.(讲义 slide 99 页内)。讲稿把这份好处接到去年那场报告上:HypeHype 因为切换顶点缓冲与索引缓冲的次数更少,从 CPU 开销里挤出了一些周期(讲义 slide 99 讲稿)。页内给出的出处是 Aaltonen, HypeHype Mobile Rendering Architecture, SIGGRAPH 2023: Advances in Real Time Rendering in Games(讲义 slide 99 页内)。这里只按页内与讲稿的口径写下这处引用,不替它添别的话。讲稿同时给出一个更朴素的理由,管理一个巨大的顶点池这件事本身大概就是好主意(讲义 slide 99 讲稿);这条理由与移动端那几条收益并列摆在同一页上,页内不给排序。
顶点动画(vertex animation)是这条路上唯一真正的坎。页内先说它是常有的东西,风是最常见的一种;每个像素要读三个顶点,模型矩阵那点乘法成本很小,可是顶点动画会迅速累加起来,页内直接把结论写成 Expensive vertex animation is a non-starter(讲义 slide 100 页内)。讲稿把这三句讲成一句因果:每个像素都要读全部三个顶点,模型矩阵的成本因此变得微不足道,一旦动画复杂,成本就会爆炸(讲义 slide 100 讲稿)。页内把这两半并排摆开,一半把每像素取三个顶点这个固定成本压得很低,另一半把动画的复杂度写成会迅速累积的那一头,落差就落在动画本身有多少活上(讲义 slide 100 页内)。
解法页内给了两行:先把顶点动画预先算好,而这个做法有内存成本、也有性能成本(讲义 slide 101 页内)。讲稿把代价摊开讲:需要跑一趟预趟、用 compute 把位置存下来,视渲染器而定,这份内存成本可能很痛;随后那一句是本章要记住的话,要么为内存付钱,要么为性能付钱,挑一个(讲义 slide 101 讲稿)。
本章可核对的结论有三条。取顶点要有一个网格池(mesh pool),做法是把全部几何放进一个大缓冲,页内短句是 Store all geometry in a large buffer,讲稿强调要的是一整个巨大缓冲(讲义 slide 98)。大池化在移动端也能少几次 API 调用,页内评价是 Hard to think of a good reason not to do it.,并引 HypeHype Mobile Rendering Architecture(SIGGRAPH 2023),讲稿说 HypeHype 因为切换顶点与索引缓冲的次数更少而省下一些 CPU 开销(讲义 slide 99)。顶点动画是这条路上唯一真正的坎,页内判定 Expensive vertex animation is a non-starter,解法是预先算好顶点动画,代价是内存成本与性能成本各一份,讲稿把它收成「要么为内存付钱、要么为性能付钱,挑一个」(讲义 slide 100、101)。另外,谈池化时那句「管理一个大池子本身就大概是个好主意」与移动端那几条收益并列在同一页,页内不给排序(讲义 slide 99)。下一章回头解释第 2 章留下的那个悬问:quad 为什么甩不掉。
10导数:为什么 quad 甩不掉
上一章把取顶点与顶点动画收在大池化与预计算上,这一章回头解释第 2 章留下的一个悬问:像素着色器为什么要以 2×2 quad 为单位跑(讲义 slide 102)。
讲义 slide 103 把因果链写得极短:GPU 渲染 quad,为什么这么做,因为导数(derivative);四条 lane 并行运行,Dx/Dy 由偏移量给出;Tex.Sample() 会隐式地从四个采样点算出 Dx/Dy,这也是它在 compute shader 或顶点着色器里非法的原因(讲义 slide 103 页内)。讲稿把 helper lane 这个名字的来源补齐:那两条 lane 之所以叫 helper,是因为它们在帮活跃 lane 算偏导数(讲义 slide 103 讲稿)。
Tex.Sample() 会隐式地从四个采样点上算 Dx/Dy。②读法(怎么读):把它读成整章的因果起点,第 2 章那笔 quad 的账在这里拿到理由。
来源:SIGGRAPH 2024 · Advances in Real-Time Rendering in Games course · Variable Rate Shading with Visibility Buffer Rendering · 讲者 John Hable(Visible Threshold, LLC)· 未修改引用
软件 VRS 因此要自己算导数。页内给的改法是一行替换:把 Tex.Sample() 换成 Tex.SampleGrad(),并把导数传进去(讲义 slide 104 页内)。讲稿同页确认这一步,要用 SampleGrad() 而不用 Sample()(讲义 slide 104 讲稿)。
第一件要拿到的量,是屏幕空间里重心坐标(barycentric coordinate)对 X 与 Y 的偏导数。页内给了来源:Schied and Dachsbacher, Deferred Attribute Interpolation for Memory-Efficient Deferred Shading, HPG 2015;并注明这段代码由 James McLaren 与 Stephen Hill 提供(讲义 slide 105 页内)。讲稿在这里交代了一段前情,它与讲义许可要分开记:他几年前在博客里写过一版,那一版里有一个错误,是 McLaren 与 Hill 联系他纠正的,页内展示的是纠正后的版本;他还说那篇博客的许可技术上是 CC0,若这有问题可以按任何许可给(讲义 slide 105 讲稿)。这条 CC0 只约束页面所引的那段代码,不改变讲义整体的非 CC 口径,也不能据此推断讲义或页内画面可以转载。
有了重心坐标的偏导数,再配合三个属性上的取值,就能算出属性的偏导数(讲义 slide 106 页内、讲稿)。要算纹理采样的导数,页内的做法是用一个 Derived_Float2 这样的结构,并为所有函数各写一份带导数的版本,除法和余弦都在其中(讲义 slide 107 页内)。讲稿把这条纪律说清:从插值量到纹理采样这条关键路径上的每一个表达式都要用这种结构把导数一路带下去,每做一次乘法、除法或别的运算,都要用链式法则(chain rule)(讲义 slide 107 讲稿)。
落到工程上有两条。其一,要确保带导数的函数被用在从插值量到纹理采样的那条路径上;讲稿举例说,原来的 uv = baseUv * scale 要换成带导数的版本,最后把 Sample() 换成 SampleGrad() 并传入解析出来的导数(讲义 slide 108 页内、讲稿)。其二,如果走材质图(material graph),要做好维护量很大的准备,页内写着 Lots of maintenance. 与 Learn to love type conversions.(讲义 slide 109 页内)。讲稿把这份代价说得更具体:要改代码生成器让它维护导数,并在带导数与不带导数的节点之间做一堆转换(讲义 slide 109 讲稿)。
替代方案是两种表面描述语言。页内说 MDL 与 OSL 两者都能做表面描述、都能算导数、都能从 MaterialX 生成;其中 MDL(Material Definition Language)出自 NVIDIA,会生成 HLSL,OSL(Open Shading Language)出自 Sony Imageworks,不生成 HLSL(讲义 slide 110 页内)。讲稿把取舍讲在这里:两者都可行,他选 MDL 的决定性一条就是它带 HLSL 后端,而 OSL 没有(讲义 slide 110 讲稿)。
最终管线页内写成 USD → MaterialX → MDL → HLSL,并附一句自我评价 Sounds more complicated than it is. 与 Easier than building a material editor, parser, generator, etc.;MDL 另有两个副产品,baking uniforms 与 baking textures(讲义 slide 111 页内)。讲稿同页说,这条管线听起来活很多,但比从零写一个材质编辑器要少得多(讲义 slide 111 讲稿)。
本章可核对的结论有三条。quad 的存在理由就是导数,Tex.Sample() 隐式地从四点采样上算 Dx/Dy,所以它在 compute shader 与顶点着色器里非法(讲义 slide 103)。软件 VRS 的改法是自己算导数,把 Tex.Sample() 换成 Tex.SampleGrad()(讲义 slide 104);第一件要拿到的量是屏幕空间重心坐标的偏导数,来源是 Schied and Dachsbacher(HPG 2015),代码由 James McLaren 与 Stephen Hill 提供,讲者博客里那一版有一个错误、是这两人联系他纠正的,而博客的 CC0 只约束那段代码、不改讲义整体的非 CC 口径(讲义 slide 105)。用 Derived_Float2 这类结构把导数沿链式法则带到纹理采样,材质图这条路维护量大(讲义 slide 107、109),替代方案是能从 MaterialX 生成、并替你算导数的 MDL 与 OSL,他的管线是 USD → MaterialX → MDL → HLSL(讲义 slide 110、111)。下一章进入四件事里的最后一件:重建。
11重建:从稀疏采样点到一张图
上一章把材质那条路径上的导数问题处理掉,这一章进入四件事里的最后一件,重建(reconstruction)(讲义 slide 112)。它的难点落在搜索上,要在不跨过边缘的前提下,给每个像素找到四个「最好」的邻居。
采样图案页内列了四条:2x Diagonal、4x Grid、25% Noise-Based、1x Everything(讲义 slide 113 页内;末条在讲义 slide 116 页内写作 1x Reference)。讲稿逐条给了读法:第一种可以理解成棋盘格;第二种是取四分之一速率、用半分辨率的格点;第三种是随机采样,把采样点偏向细节更多的地方;最后一种就是把每个采样点都渲染一遍(讲义 slide 113、114、115、116 讲稿)。
重建里最关键的一个技巧是准确的边缘检测(edge detection)。页内说它分水平与垂直两个方向、存成位掩码(bitmask);判据用三项,Instance ID、Depth 与 Geometry Normal(讲义 slide 117 页内)。讲稿给了评价:把实例 ID、深度与几何法线一起用,就能得到一个相当好的边缘检测器(讲义 slide 117 讲稿)。
选邻居这一步,页内说要给每个像素挑出「最好」的 4 个像素,做法是对每个像素看四个区域,对角线按顺时针走,再按距离排优先级(讲义 slide 118 页内)。例子页把一页的情形写清:灰像素是起点,蓝像素是合法邻居,红边是非法边界,要找最近的邻居、并且不要跨过边界(讲义 slide 119 页内)。这一页与下一页在页内是内容相同的逐格揭示连拍(讲义 slide 119、120 页内)。
搜索这一段的核心是一条让步。页内说搜索有很多做法,洪泛填充(flood fill)太贵,需要更快的算法(讲义 slide 121 页内)。给出的办法是一条预先固定的硬编码搜索路径,页内把好处写成 Same result in most cases 与 Much simpler,并问搜索该用 for 循环还是布尔表达式(讲义 slide 122 页内)。讲稿在这里把代价讲明:这条路径理论上会漏掉通往某些像素的合法路径,但如果一个像素需要转好几次直角才能找到,它本来就算不上好候选(讲义 slide 122 讲稿)。下一页把这条让步收成一句,只搜这一条路径,忽略其他路径(讲义 slide 123 页内)。
一条路径要算合法,页内给了三条判据:路径上不能有别的命中,水平边必须合法,垂直边必须合法;三者之外再把整件事写成一个布尔表达式,页内注明它与 EQAA 思路相近(讲义 slide 124、125、126、127、128 页内)。讲稿补一句,判断这件事最好的方式是布尔表达式,不需要 for 循环(讲义 slide 128 讲稿)。
实现上把布尔量换成位掩码。搜索半径是 4 个像素,因此一次能搜 24 个像素,左右各留 4 位垫;每个搜索方向得到一个 [-4,4] 之间的值,一个轴占 4 位,四个方向正好装进一个 u32(讲义 slide 129、130 页内)。页内给了读数与自评:Very generic,边缘检测与搜索合计约一百微秒,口径是 Geforce 3070、@1080p,后面跟着一句 TODO: Optimize(讲义 slide 130 页内)。讲稿把这条自评说得更直:它比我希望的慢,优化那一趟在他的待办上(讲义 slide 130 讲稿)。
收尾演示是逐格揭示的五页,依次给出选中的像素、垂直边、水平边、重建帧与 TAA;重建帧里少数找不到合法邻居的像素被标成红色,页内说明 TAA 必须处理这些无效像素(讲义 slide 131、132、133、134、135 页内)。讲稿说,要改 TAA 算法,让当前帧里连一个邻居都没找到的像素不被采信(讲义 slide 135 讲稿)。
本章可核对的结论有三条。采样图案有 2x Diagonal、4x Grid、25% Noise-Based 与 1x 四种(讲义 slide 113、116)。边缘检测分水平与垂直、存成位掩码,判据是 Instance ID、Depth 与 Geometry Normal(讲义 slide 117);选邻居要给每个像素挑出最好的 4 个,四个区域、按距离排序(讲义 slide 118)。搜索这一段的关键让步是硬编码路径,页内写成 Only search this one path,讲稿承认它理论上会漏路径;实现把布尔量换成位掩码,半径 4 像素、一次搜 24 像素、左右各留 4 位、四个方向装进一个 u32,页内读数是在 Geforce 3070、1080p 下约一百微秒,并写着 TODO: Optimize(讲义 slide 123、129、130)。下一章把这些数字放回整条管线的账里。
12性能、结论与边界
上一章把重建收在一条硬编码搜索路径与位掩码实现上,这一章用两套性能数字把它放回整条管线的账里,并交代结论与边界(讲义 slide 137)。
先给一处前沿限定。页内写的是完全可变速率(fully variable rate)在光照变化时会有鬼影(ghosting)问题,因为 TAA 的颜色夹取(color clamp)变得不可靠、参与夹取的邻接像素每一帧都在变,页内自陈需要不断调参;1x、2x、4x 这几档图案工作正常(讲义 slide 136 页内)。页内另给历史采样选择作替代,引 Drobot, Software-based Variable Rate Shading in Call of Duty: Modern Warfare, SIGGRAPH 2020(讲义 slide 136 页内)。
合成测试是 2021 年做的,页内写明 Large and small tris、尺寸大致一像素一个,用一份复杂材质加一个细分到能控制三角形尺寸的四边形(讲义 slide 138 页内、讲稿)。大三角形那一档 Forward 与 Deferred 都不错,VBuffer 因为多付开销而略慢,页内写作 VBuffer is slightly slower due to overhead(讲义 slide 139 页内)。到 Tiny Tris(1 pixel)账翻过来:页内写明一像素三角形下 Forward 与 Deferred-Material 都有严重的 quad overdraw 问题、VBuffer 为高密度几何付的代价更小,读数是 Forward 的 Material 8.96、Other 1.59、Total 10.55,Deferred 的 4.65/0.77/1.59/7.01,VBuffer 的 1.70/0.84/2.18/4.72,单位为毫秒;页内另注高密度几何会带来更多带宽(bandwidth)占用与取顶点时的缓存未命中(cache miss)(讲义 slide 140 页内)。讲稿读成 VBuffer 是明显的赢家(讲义 slide 140 讲稿)。
真实场景是 March of the Monkeys,页内写明网格被细分成约 100 万三角形、用 meshoptimizer 自动生成 LOD,没有网格处于 LOD 0,有 6 个在 LOD 1(也就是 50 万三角形);材质着色器极其简单、平坦区域多;Depth Prepass 始终开启,页内注明这是 DBuffer decal 所需;光照是一盏带 4 级级联(cascade)的太阳光加一盏聚光灯;口径是 NVIDIA 3070、1080p、DX12(讲义 slide 144 页内)。汇总表只算材质加光照,读数是 Forward 2.47、Deferred 1.94、Vis 1x 1.67、Vis 2x 1.44、Vis 4x 1.35、Vis 4x (Variable) 1.47(讲义 slide 151 页内)。讲稿读成 VBuffer 在 1x 就已经更快而且看起来一样,而可变速率那两张收益很小(讲义 slide 151 讲稿)。拆项页给出 1x 的 .45/.70/.38、2x 的 .29/.36/.79、4x 的 .20/.20/.95、Variable 的 .21/.21/1.05,页内脚注说明 Actual marker to marker was 0.84, but adjusting to 0.70 because NSIGHT showed a gap,并留着两条自评 Significant room for optimization. 与 Was a bit pressed for time.,以及一句 Lower rate scales well for more complex materials.(讲义 slide 152 页内)。讲稿补一句 4x 那一档的边界,到 4x VRS 活已经不够多、GPU 喂不饱(讲义 slide 152 讲稿)。
结论那一页把两栏并排摆开,起头分别是 VBuffer is better… 与 GBuffer/Forward is better…,下面列着 Low Triangle Density、High Triangle Density、Low Material Graph Complexity、High Material Graph Complexity、Weak GPUs、Powerful GPUs 等项,另有 VBuffer overhead is significant 与 VBuffer overhead is minimal 两句,页内最后一行是 GPUs get more powerful every year.(讲义 slide 153 页内)。这张两栏表的行归属未经核对,正文只按讲稿口径写:密集三角形加复杂材质、高端 GPU 上 VBuffer 明显更好,场景不复杂、GPU 较弱时不划算(讲义 slide 153 讲稿)。
个人意见这几页从 Personal Opinion 与 Agree/disagree, decide for yourself. 起头,页内问 Is VBuffer the better approach?、答 VBuffer is the better approach…eventually.,随后补 But eventually can be a long time. 与 Games still shipping on PS4/XBox1,把问题写成 When is the transition point for VBuffer? Not if.;页内也提醒,切到 VBuffer 就要长期同时维护旧的 GBuffer/Forward 路径,并把 Long Time 等于 Forever(讲义 slide 154、157 页内)。讲稿明说这是他非常主观、非常可能有偏见的意见、该带着怀疑听,正确的问法是过渡点在哪里(讲义 slide 154 讲稿)。
讲者在结束前还用一段加场讲了 OIT(顺序无关透明);讲稿说明为 VBuffer 配上 VRS 之后再加 OIT 是容易的,并明说这是一份原型、他 wouldn't call this a finished product, but it's an interesting experiment that shows promise.(讲义 slide 158、159 页内、讲稿)。这一场不在本篇标题范围内,这里只作一段提及,不展开、不取图。
本章可核对的结论有两条。完全可变速率在光照变化时会出鬼影、需要不断调参,可靠的只有 1x/2x/4x 这几档固定图案(讲义 slide 136)。两套数字不可混用:合成测试里一像素三角形的合计是 Forward 10.55、Deferred 7.01、VBuffer 4.72 毫秒,March of the Monkeys 里材质加光照合计是 2.47/1.94/1.67/1.44/1.35/1.47 毫秒(讲义 slide 140、151)。
边界 1: 它给的是一条「到处都够好」的统一路径,页内没有承诺它在所有场合里最优(讲义 slide 54 页内原话:One solution for all platforms — "Good enough everywhere");mesh shader 与软件光栅器可作后续优化,不能当作统一路径(讲义 slide 66)。
边界 2: 本篇的实测数据来自一份公开发布的第三方调查:三平台读数出自 Sebastian Aaltonen 的推文(讲义 slide 67–71),不由讲者本人测得;页内另注明 NVIDIA 那台的基线在推文里缺失,只靠后来一版 NSIGHT 抓取与 Leading Vertex 相符来补。
边界 3: 性能数字有两套、不可混用:2021 年的合成测试(讲义 slide 138–140)与后来的 March of the Monkeys(讲义 slide 144–152);讲者为了对比跳过了 prepass 与阴影这些在各变体里开销相同的部分。
边界 4: 页内表格的数值与行列对应已逐格核对(讲义 slide 35/47/68–71/95/140/151/152 共七处):核对方式是从 PDF 里按表格边框取出单元格,再逐格比对正文引用的数字与页面归属 —— 这次核对同时发现并修掉一处页码引错,带 Software VRS 第三列的对照表在讲义 slide 36,slide 35 上那张只有 Regular 与 2x2 VRS 两列。正文仍不逐行抄表,只引讲者点名说出的数字与结论。
边界 5: 完全可变速率目前不可靠:光照变化时会出鬼影,因为 TAA 的颜色夹取邻域每帧都在变,页内自陈需要不断调参;能可靠用的是 1x/2x/4x 这几档固定图案(讲义 slide 136)。
边界 6: 重建那一趟的性能是讲者自己承认的短板:页内写着 TODO: Optimize,讲稿说它比希望的慢(讲义 slide 130),正文不把它写成已经足够快。
边界 7: 许可:讲义页脚在 185/185 页上印同一行保留所有权利的版权行(© 2024 SIGGRAPH ADVANCES IN REAL-TIME RENDERING IN GAMES course. ALL RIGHTS RESERVED.),按非 CC 口径逐件登记、逐图署名并标注残留风险;另有一处要与许可分开写,讲义 slide 105 讲稿说那段重心坐标导数所引的博客文章许可是 CC0,那是页面所引代码的出处许可,不改讲义整体的非 CC 口径,也不能据此推断讲义或页内画面可以转载。
边界 8: OIT 那一段不在本篇标题范围内:讲者把它讲成「结束前的加场」(讲义 slide 158 讲稿原话),页内以 OIT is easy. 与 The hard part is everything else. 定位,并明确这是一份原型、不算成品(讲稿原话 I wouldn't call this a finished product, but it's an interesting experiment that shows promise.);正文只作一段提及,不展开、不取图。
13贡献块
可搬走的四条与站内互读
这一场能留下的东西,集中在「把一条硬件的既定行为拆开看清楚,再问哪一半可以自己拿走」这一类:quad 由硬性规定变成可绕开的开销、primitive ID 由语义变成可离线排布的数据、导数由隐式求取变回显式传入。讲者在 slide 48 把整篇的口气交代得很清楚 —— 结构上只是多一趟的 GBuffer,实践上复杂得多。本章收的四条,没有一条是「设计了一套新方案」,全部是「在一处既定的做法上换了选择、换到了多少」。
贡献 1:把一个关于眼睛的判断,翻译成一条可执行的采样策略。 论据是感知侧的:我们看不到像素、看到的是梯度,而眼睛本身就是用这些梯度拼出图像的一张神经网络(slide 4)。推论分两步。第一步是优先级 —— 边缘最强、优先保;细节次要(slide 9)。第二步是结构判断 —— 要让「边缘保、内部省」成立,必须把几何率与着色率分开,否则省采样率的同时也把边缘一起模糊掉;页内给出的代价写成一行 Upsampling sacrifices edges —— Tradeoff is detail vs performance(slide 10)。可搬走的是这条链条的形状:先想清楚「观者真正在看什么」,再去问「现在这套管线里哪一层把它和别的东西绑在一起」。这一场后面所有的工程量,都是在为第二步付账。
贡献 2:把「统一路径」当成一等约束,而不是先挑最快的。 选型页把它们写死了:One solution for all platforms —— "Good enough everywhere"、Can add optimized solutions later、No major performance cliffs(slide 54)。于是即便页内同时列出 Mesh Shaders(顶点性能峰值、内存最好)与 Software Rasterizer(在某些硬件上最快)这两个明显更快的选项,它们仍被划到 不可用作统一路径那一栏,理由分别是不是所有平台都支持与不是所有平台支持 64 位原子、需要一个平台无关的回退、移动端有电池问题(slide 63–66);Geometry Shader 页内只留一句 Very inconsistent。可搬走的是这条判断:当交付面横跨多代硬件时,「哪里都不会崩」比「某个平台上最快」更值钱 —— 快的那些留给后面的优化,不进第一版。
贡献 3:把一条硬件语义变成一份可以离线算好的数据。 Leading Vertex 的原理只有一行 —— 用 flat/nointerpolate 模式,整个三角形只取一个顶点的颜色(slide 76),而这个「第一个顶点」在多数平台上是第一个、在 OpenGL 上是最后一个。这条平台差异本来要留在运行时判断;他们把它挪到离线:Step 1 先贪心地给尽可能多的三角形定下起点(选可用顶点里价数最低的那个),Step 2 再对剩下的做一次深度优先搜索、把起点旋进来(slide 77/91)。离线做完的收益是双份的:多数情况下第二步根本不用跑,而且每簇只要 64 个重排索引、每三角形只存两个 6 位索引,32 位索引下 5.5 字节/三角形(对照未压缩的 12 字节)、16 位索引下 3.5 字节/三角形(对照 6 字节,slide 95)。可搬走的次序是:当一条语义在不同平台上有分歧时,把分歧消解在离线数据里,而不是消解在运行时的分支里。代价也一同写在页内:对每个不是起点的顶点都要从簇里丢掉一个三角形,最常见的失败是一个三角形通过单个索引松散地连在簇上(slide 92–93)。
贡献 4:承认哪一块甩不掉,然后绕开它。 2×2 quad 不是历史包袱,它存在的理由是导数 —— 纹理采样隐式地用四个样本估出 Dx/Dy,这也是它在计算着色器与顶点着色器里非法的原因(slide 103)。既然甩不掉,就把求导显式化:自己算导数、把 Tex.Sample() 换成 Tex.SampleGrad()(slide 104),并沿从插值量到纹理采样的那条路径用链式法则把导数带下去(slide 107–108)。可搬走的是这种分工意识:遇到一条「硬件就是这么做的」时,先问它是为了满足什么,再决定是自己接管那一部分、还是接受它。同一条纪律在重建那一节反过来用了一次 —— 搜索本可以严格求解,但洪泛填充太贵,于是换成一条预先固定的搜索路径(slide 122–123),讲者同时交代了让步:理论上会漏掉合法路径,但需要转好几次直角才能找到的像素本来就不是好候选。重建那一趟页内自评 TODO: Optimize、讲稿说它比希望的最简慢(slide 130),而完全可变速率在光照变化下会出鬼影、需要不断调参,可靠的是 1x/2x/4x 这几档固定图案(slide 136)—— 短板写在页上,不藏。
站内可以接着读的几篇。篇目 37(HypeHype 的随机分块光照)与本篇有一处直接相邻:本篇讲 Mesh Pool 的顺带收益时,页内引的正是 HypeHype Mobile Rendering Architecture(SIGGRAPH 2023,slide 99),讲稿说那台渲染器因为切换顶点与索引缓冲的次数少而省下了一些 CPU 开销 —— 也就是说,「把所有几何放进一个大缓冲」这件事,站内已经有一篇从移动端 CPU 成本的角度写过,两篇连读能把「池化」的收益面看清。篇目 35(NanoMesh 的移动端自适应 LOD)与本篇是一组互补:它把 cluster 层级与 LOD 挑选完整地放在移动端约束下做,而本篇明说 mesh shader 不能当统一路径(slice 63–66)、有些平台需要多条渲染路径、又不能在磁盘上存三份(slide 96)—— 两篇合起来回答的是同一个工程问题:同一份网格数据要同时喂给几代硬件时,走哪条路。两篇的平台口径不同(一边是移动端、一边是 3070 与 DX12),读数不可横比。篇目 40(CBT 的大规模组件三角化)与本篇同属几何侧,但切的是另一个问题:那篇解的是「三角化状态怎么记、怎么分配」,本篇解的是「着色率怎么与几何率分开」;两篇的距离恰好说明几何侧这条轴上还留着两块彼此独立的地。至于篇目 39(Frostbite 的动态漫反射 GI)与本篇不同轴,一个管光照、一个管光栅化与着色率,没有可横比的读数。本篇讲义未给代码仓库或开源链接;页内出现的 github.com/zeux/meshoptimizer 与 cg.ivd.kit.edu 那两条分别是簇化工具的出处与所引论文的出处,不是本场实现的开源地址。
14原文出处块
原文出处与引用口径
出处:SIGGRAPH 2024 · Advances in Real-Time Rendering in Games course · 2024-08 · 讲者 John Hable(Founder, Visible Threshold, LLC)· 官方讲义《Variable Rate Shading with Visibility Buffer Rendering》,185 页,720×405 pt(16:9),来源 PDF 2024-siggraph-advances-vrs-visbuffer.pdf。讲者身份直接印在页内(slide 1 给出姓名与头衔,并列出此前供职的 Electronic Arts、Naughty Dog、Epic、Unity 等),不需要另据公开资料补齐;slide 183 给出主页 www.visiblethreshold.com 与邮箱。
材料:官方讲义 185 页 —— 整页即幻灯片、全出血(16:9),页内有实时文字层(185/185 页),页内文字合计 38,915 字符(已排除页脚那行版权行);另有一份同源的 pptx(185 张幻灯片,其中 184 张带讲者备注、合计 40,132 字符),本篇的讲稿引文取自该备注,取图仍走 PDF。页脚在 185/185 页上印同一行保留所有权利的版权行,整页渲染会把它带进每一张图;处置见下。页码为 PDF 页序(从 0 起),引用写作「讲义 slide N」。一处形态事实:页内那张 1536×864 的位图是跨页共享的同一张模板(同一 xref 出现在 175 页、字节完全相同),因此原生图流不成立,取图走整页渲染(2.2222× → 1600×900)。
许可:讲义页脚在 185/185 页上印着 © 2024 SIGGRAPH ADVANCES IN REAL-TIME RENDERING IN GAMES course. ALL RIGHTS RESERVED.。取图走整页渲染,这行会进入每一张图;处置是原样保留、不裁切、不改像素 —— 裁掉等于把版权行从图里抹掉,对署名诚实性不利,也违反「未修改引用」口径。逐页检索 Copyright、Creative Commons、CC BY、CC0、License、licensed、Images ©、with permission、courtesy of、许可,除页脚那行之外,只在少数页检索到参考文献的出处标注(例如 Münstermann, Krumpen, Klein, and Peters, Moment-Based Order-Independent Transparency, I3D 2018 与 https://cg.ivd.kit.edu/english/mboit.php),那不是素材许可。故按分区非 CC 口径处理:逐件登记、逐图署名(图号 + slide 号 + 出处 + 「未修改引用」)+ 标注残留风险(讲义未授权转载,条款以官方发布为准)。有一处必须与讲义许可分开写:讲者提到那段重心坐标导数的博客文章许可技术上是 CC0(slide 105 讲稿原话大意:Technically the license on the blog post is CC0 but if that's an issue just ping me and you can have it under any license you want)—— 那是页面所引代码的出处许可,不改讲义整体的非 CC 口径,也不得据此推断讲义或页内画面可转载。
引用口径:页内数值按讲义原值写,不改写、不改位数、不四舍五入到别的位数 —— quad 三档账:大三角形 1x、约 10 像素 2x、一像素 4x(slide 26);三列对照:Large 1x/0.25x/0.25x、约 10 像素 2x/1x/0.25x、一像素 4x/4x/0.25x(slide 36;slide 35 上的同名对照表只有 Regular 与 2x2 VRS 两列);三条路径代价:Forward 整趟 4x、Deferred GBuffer 写 4x 与光照 1x、VBuffer 一律 1x(slide 47);三平台读数:NVIDIA 0.883/1.367/1.750(slide 68)、AMD RDNA1 2.06/2.16/3.78/5.45(slide 69)、Intel Gen12 12.63/12.83/12.87/20.17(slide 70);折算倍数:Leading Vertex 1x/1x/1x、SV_PrimitiveID 1.5x/1.8x/1x、Non-Indexed 2.0x/2.6x/1.6x(slide 71);簇规格 64 三角形/64 顶点、50 顶点/14 个复制(slide 92);打包账:每三角形两个 6 位索引,32 位索引 5.5 字节/三角形(对照 12 字节)、16 位索引 3.5 字节/三角形(对照 6 字节)(slide 95);重建:搜索半径 4 像素、一次搜 24 像素、左右各 4 位垫、结果落在 [-4,4]、一个轴 4 位、四个方向装进一个 u32、约一百微秒、Geforce 3070 @1080p(slide 129–130);合成测试:Forward 8.96/—/1.59/10.55、Deferred 4.65/0.77/1.59/7.01、VBuffer 1.70/0.84/2.18/4.72(slide 140,毫秒);Monkeys 场景:网格约 100 万三角形、6 个在 LOD 1(50 万三角形)、NVIDIA 3070、1080p、DX12、太阳光 4 级级联加一盏聚光灯(slide 144);汇总 2.47/1.94/1.67/1.44/1.35/1.47(slide 151);拆项 1x .45/.70/.38、2x .29/.36/.79、4x .20/.20/.95、Variable .21/.21/1.05,脚注 marker to marker 0.84 调到 0.70(slide 152)。三处页内自评照写:slide 130 的 TODO: Optimize、slide 152 的 Significant room for optimization. / Was a bit pressed for time.、slide 68 的 Baseline missing from tweet 与 Not easy to fit nuanced graphics experiments into 140 characters。讲者的让步与限定一并保留:对 quad overdraw 的算法做了简化(slide 26 讲稿);软件 VRS 要付额外开销(slide 36);VBuffer 的 Downside 是 Extra overhead(slide 47);实践上复杂得多(slide 48);目标是 "Good enough everywhere"(slide 54);mesh shader 与软件光栅器能作优化、不能作统一路径(slide 66);失败情形是每个非起点顶点要丢一个三角形(slide 92–93);有些平台需要多条路径、不能在磁盘上存三份(slide 96);昂贵的顶点动画是 non-starter、内存与性能只能挑一个(slide 100–101);他博客那版重心坐标导数有错误、是 James McLaren 与 Stephen Hill 纠正的(slide 105);材质图维护量大(slide 109);硬编码搜索路径理论上会漏掉合法路径(slide 122);重建那一趟慢、待优化(slide 130);完全可变速率有鬼影、需要不断调参(slide 136);4x 下 GPU 喂不饱(slide 152 讲稿);结尾是主观、可能有偏见的个人意见,并把问题改写成 When is the transition point for VBuffer? Not if.(slide 154/157)。数据来源要写清:三平台实测出自 Sebastian Aaltonen 公开发布的推文(slide 67–71),不是讲者本人测得。页内表格不逐行抄(slide 35/47/68–71/95/140/151/152 共七处);slide 153 那张两栏对照表的行归属已逐格核对 —— 「VBuffer is better」一栏是 High Triangle Density、High Material Graph Complexity、Powerful GPUs(其下注 VBuffer overhead is minimal),「GBuffer/Forward is better」一栏是 Low Triangle Density、Low Material Graph Complexity、Weak GPUs(其下注 VBuffer overhead is significant),与讲稿口径一致;正文按核对结果写,仍不声称逐行对应。不与任何商业方案横比:slide 180 参考文献里出现的 Nanite 只作文献列出,不展开、不评判。OIT 那一段按裁定只作一段提及(slide 158–177):页内把它讲成 OIT is easy. —— The hard part is everything else.,并强调那是原型、不算成品(I wouldn't call this a finished product, but it's an interesting experiment that shows promise.)。篇幅说明:正文按 15,000–20,000 汉字写;slide 0–2 的自我介绍与公司介绍只作定位,slide 178–179 的致谢只列名单,slide 180–182 的参考文献只作出处,slide 183 的联系方式与 slide 184 的提问页不入正文。