"Hello, I'm Sergei Kulikov from Machine Games and this talk is about strand-based hair in Indiana jones and the Great Circle."
- 先立指标,再谈做法:叙事驱动的第一人称游戏,约 4 小时过场、玩家几乎总在近距离看脸,角色必须尽量好看(讲义 slide 3);硬指标是全平台锁 60 FPS、最低规格最坏情形(十几个角色在屏)约 2 毫秒 GPU 预算、不做卡片回退、允许为性能偏离物理正确(讲义 slide 4)。
- 工作流换轨省掉两步:卡片流程要制卡、烘贴图、摆放雕刻、调网格法线、导出、调材质;发丝流程只需给样条造型、导出、调材质 —— 省掉制卡与调法线,每次迭代明显更快(讲义 slide 5)。
- 头发被塞进一帧的空隙里:模拟留在主图队列(必须在不透明光照之前完成),光栅化与顶点着色放 async compute,与不透明光照和漫反射 GI 重叠;合成与其他透明一起背到前(讲义 slide 10)。关掉 async 时四台主机的合计是 1.64–2.39 毫秒,async 平均补回约 0.5 毫秒才落进预估(讲义 slide 12)。
- LOD 不另做模型,段数用三层上限钉死:随机化发丝顺序后截取一段并按比例加粗以保持外形(讲义 slide 14);载入时封单资产发丝数(65k / 32k 两档)、每帧维持「每 64×64 贴片不超过 2000 段」、全区合计压到 200 万段以内(讲义 slide 15、16)。
- 可见性缓冲是全场最贵的写目标,所以一路砍:3 个有序层加 1 层累加覆盖率,64 位每层在 1080p 要 56 MB;压到 32 位载荷(22 位段索引 + 10 位深度),深度在模型包围盒内归一化;整屏换成 64×64 贴片图集,放不下就逐贴片降分辨率(讲义 slide 26–29)。
- 分歧与缓存两笔账:把两层循环压成一层去掉厚度与斜率分歧,再用像素请求栈把贵的全局写入攒满一个 wave 统一执行,活跃 lane 从不到一半升到约 80%、着色器快近一倍(讲义 slide 36、38、41);为 LOD 做的随机化让缓存命中率低,于是加一次廉价的全局计数排序(多数硬件不到 100 微秒,讲义 slide 43)。
- 自阴影试了三条路,最后发货的是离线预计算:深透明度图因内存与「每灯再光栅化一遍」被否(讲义 slide 48),沿光 raymarch 密度体不占内存但缺高频、部分发型需 256³(讲义 slide 49),随机方向追踪加球面探针与灯数解耦但在 Series S 上单个发型就要 6 毫秒(讲义 slide 51);最终用软件光追离线烘一份可见性球面函数,运行时一次查表对所有灯生效(讲义 slide 53),并把「散射事件数」编进 L1 球谐、给未命中射线加负偏置(讲义 slide 61)。
- 结论是一句工程纪律:发丝在当代硬件够跑 60 FPS,但要挑战场、不能全都要,给每一部分设好最坏情形上界才是可扩展的关键(讲义 slide 85)。
1「让角色好看」与工作流换轨
「让角色好看」与工作流换轨
在这款游戏里,让角色好看是一条硬需求。讲者 Sergei Kulikov(MachineGames Sweden AB)登上讲台后先把动机讲清:Indiana Jones and the Great Circle 是第一人称的动作冒险游戏,叙事驱动,过场动画合计约 4 小时,第一人称视角又让玩家几乎总是近距离看到敌人的脸(讲义 slide 3)。人物的脸被反复放到镜头前,角色就必须尽量好看,游戏里的近身格斗同样发生在第一人称视角下。第二个动机落在美术这一侧:hair card(头发卡片)的创作流程很费时间,团队希望把美术从重复的技术步骤里解放出来,把时间让给创作。讲者还先谢了一圈同事(讲义 slide 2):他把发丝系统说成一次贯穿项目全程的协作,id Software 引擎团队与 MachineGames 的引擎与角色美术团队都参与其中,并特别点到两件事,Michael Wynne 把团队引上 strand-based hair(发丝渲染)这条路并做出第一版可用实现,Jorge Luna 负责头发模拟。这次的时间只够覆盖目标与要求、系统总览以及光栅化、着色、合成三大块,头发模拟与头发路径追踪不在今天的范围里(讲义 slide 1)。
指标被写成一页(讲义 slide 4)。目标平台是 Xbox Series X/S、PS5 与支持光追的桌面 GPU,全部锁定 60 FPS,讲者把这条称为清晰的硬目标,所有系统都得在全部目标平台上跑到 60 FPS。整个系统(模拟加渲染)在最低规格、最坏情形下的 GPU 预算估为约 2 毫秒,最坏情形指屏上约有十几个角色、其中一部分贴近相机,更简单的场景当然希望更快。团队不想为每个头发资产做两版,所以系统必须够快够稳,能到处用;换轨之后只剩极少数资产仍用卡片,主要是动物。在满足前面两条的前提下,他们想要负担得起的最好画质,也允许为了性能偏离物理正确。
创作流程的差别被并排放在一页上(讲义 slide 5)。卡片这条路要先把每张卡片上的发丝建出来(用 xGen 或手做),在 Substance Painter 里烘贴图,然后靠摆放与雕刻卡片把造型做出来,接着改网格法线,从基本形体或改过的头部几何上投影法线,再导出到引擎里调材质。发丝这条路只需要直接用样条造型,团队用的是 xGen Interactive Groom,讲者说也可以用 Houdini 或 xGen,导出之后再调材质。卡片流程里的制卡与调法线这两步被整段省掉,每一次迭代都明显更快。讲者补了一句分量很重的话:两条流程的差别大到无法从其中一条自动生成另一条。这句话解释了为什么「不做卡片回退」会是指标的一部分,回退意味着要同时维护两套内容,省不了一步开关。
覆盖范围在同一批页面里交代(讲义 slide 6)。全部角色 groom(发型资产)与面部毛发都用发丝做,讲者说他印象里整款游戏只剩两具尸体仍用头发卡片;服装上的部分毛皮元素也用发丝,他举的例子是冬装外套上的毛帽与毛领。
动物毛是唯一没有完全成功的一块(讲义 slide 7)。这一页把蜘蛛、猴子、猫列在动物毛之下,讲者说他们确实把发丝用在了部分动物上,比如蜘蛛与这只猴子,游戏里能看到的狗仍用头发卡片。他留下一个悬念:有一些问题挡在他们面前,让发丝没能覆盖全部动物,这些问题后面会展开。手与手指上的绒毛属于项目后期的补充,团队对结果相当满意。
本章可核对的结论有两条。指标是死的:全平台 60 FPS、最坏情形约 2 毫秒 GPU 预算、不做卡片回退、在能做的范围内争取最好画质并允许为性能偏离物理正确(讲义 slide 4)。工作流换轨省掉的是制卡与调法线两步,收益主要在迭代速度上(讲义 slide 5);两条流程无法互换,卡片回退因此被提前排除。发丝覆盖了全部角色发型、面部毛发与服装上的部分毛皮元素,动物毛只部分成功,狗仍留在卡片一侧(讲义 slide 6、7)。
2引擎与一帧里的位置
上一章把指标与工作流立住了,这一章要把头发放回它所在的环境里,先看引擎给了什么约束,再看它被安放在一帧的哪一段。
引擎这一段写在讲义 slide 8。MOTOR 派生自 idTech 7,与 idTech 共享大量渲染代码。直接光照走聚簇前向,一部分不透明 pass 走延迟,讲者举的例子是漫反射 GI,游戏里还用到实时光追的漫反射 GI。GPU 内存大多在启动时静态分配,游戏过程中不能突破这些上限,这条约束会一路影响后面的取舍,凡是常驻的数据都得省着用。引擎里没有供美术编辑的着色器图,全部着色器由图形工程师手写,目的是把对代码与性能的控制握在手里。引擎大量使用 async compute,一帧的大部分都被异步工作覆盖。讲者把这些前提放在开头,是因为后面每一个取舍都落在这套前提里。
一帧的骨架摊在讲义 slide 9 上。这一页把一帧画成两条队列,图形队列与 async compute 队列并排,梳出三大块:公共 pass(GPU 场景收集、GPU 蒙皮、遮挡剔除、阴影贴图、上一帧的后处理、光追 BVH 构建、GI pass)、不透明渲染(不透明深度、头发模拟、前向不透明光照、GI 与 SSS 与 SSR、不透明光照合成)、混合渲染(混合几何的前向光照、粒子模拟与光照、头发渲染、水渲染、混合光照合成)。讲者提醒图上的横条宽度不表示真实耗时,只表示各段相对邻居队列的位置。对本篇要紧的是三大块的分法,以及他们把混合几何的工作尽量提前,让一部分在主线画不透明像素时就完成,用 async compute 去填一帧里的空隙。讲者按备注把这三大块叫公共 pass、不透明像素的光照 pass 与混合几何渲染,三块的划分方式决定了头发能插进哪里。
七阶段(讲义 slide 10)把这条管线拆得更细。CPU 准备只做三件事:收集视野里的头发模型、算出各自的 LOD(细节层次)、发出 dispatch。模拟更新顶点位置与密度体素网格,这一段逐资产可选。skinning(蒙皮)把发丝蒙到三角形上并写入变换后缓存。剔除做视锥剔除、遮挡剔除与帽子剔除,写出可见段列表。光栅化用软件光栅器,每个 lane 一段,写进可见性图集。着色发生在发丝空间,每段一个着色点,写进独立缓冲。合成给每个发型资产画一个四边形,背到前排序,与其他透明物体一起处理,写出深度与运动矢量。调度理由由讲者补在备注里:模拟必须在不透明光照之前做完,所以留在主图队列;光栅化与顶点着色挪到 async compute,与主线的透前向光照和漫反射 GI 重叠。他给的理由是头发负载可预测且均匀,正好填补 GPU 利用率上的空隙。讲义 slide 11 在文本层与备注里都只有页码,没有内容可引。
计时表(讲义 slide 12,页内表)给出四台主机的分 pass 读数,口径是关闭 async compute。Series S 跑 1080p,Series X 与 PS5 Base 跑 1800p,PS5 Pro 跑 2160p。模拟为 0.49、0.36、0.36、0.29 毫秒,蒙皮 0.17、0.14、0.14、0.14,剔除 0.10、0.09、0.08、0.07,光栅化 0.61、0.55、0.55、0.45,着色 0.62、0.40、0.39、0.26,合成 0.40、0.40、0.42、0.43,合计 2.39、1.94、1.95、1.64 毫秒。讲者从这张表里读出两件事。关闭 async compute 时 Series S 的合计 2.39 毫秒已经超出 2 毫秒预算;开启 async compute 之后测得的数字平均补回约 0.5 毫秒,才落进他们的预估。其余平台各 pass 之间的相对耗时有差别,但合计落在相近的量级。把这张表与本篇的三段分法对照,光栅化与着色是两块最大的开销,Series S 上两段合计 1.23 毫秒。这张页内表的行标签与数值在 PDF 里是散块,能自洽对齐的部分按上面列出,行与列的完整对应关系未经人工核对。这张表把七阶段里每一段的代价摊开,供后面逐段优化时对照。
本章可核对的结论有两条。头发被切成三段安置在一帧里:模拟留在主图队列,光栅化与顶点着色进 async compute 与不透明光照和漫反射 GI 重叠,合成跟其他透明一起收尾(讲义 slide 9、10)。计时上,关闭 async compute 时 Series S 合计 2.39 毫秒,超出 2 毫秒预算,开启后平均补回约 0.5 毫秒才落进预估(讲义 slide 12,页内表)。
3LOD 与段数上限:把预算钉死
在讲 LOD 之前要先放一段早期读数,因为它常被拿来当起点。项目最早的一次性能测量是屏上 2 个头发模型、逐像素光照、简单软件光栅化,在 Series X 上剔除加光栅化要 8.4 毫秒、着色加合成要 2.6 毫秒(讲义 slide 13)。讲者点明当时多数视觉特性已经就位,只差头发投在皮肤上的影子,所以他们面前的路还很长。这段读数属于项目早期,与讲义 slide 12 的计时表不是同一时期,两者不能并列成前后对比。
LOD 的做法写在讲义 slide 14,它没有沿着「为每一档单独建一个模型」的路走,改用随机化发丝顺序,再只取发丝缓冲的一段来用,出处是 [Taillandier20]。发丝被截掉之后,按比例把留下的发丝加粗,外形看上去仍然一样。这样算下来,需要计算的只剩每个模型该用多少根发丝。这个数分三段来定。第一段在资产载入时按画质档封住单资产的发丝数上限,小资产不减,因为它们对渲染时间贡献不大,减了反而更看得出来。第一阶段只处理发丝数,后面两阶段处理段数,两者分开是因为发丝数在载入时就能定下来,段数要每帧按画面重新算。
第二段是发丝数本身的上限,讲义 slide 15 给了两档:PC 高配、Xbox Series X 与 PS5 上限 65k 根,PC 低配、Xbox Series S 与 Steam Deck 上限 32k 根,低的一档是他们在算力较弱的硬件上用的最低画质设置。这条上限按画质档设定,画质档本身又与硬件挂钩。讲者说两档的视觉结果接近,低档看起来略暗一点。他把这个偏暗归到 self-shadowing(自阴影)的做法上,具体原因留到后面解释,这里先记住有这层关联。讲义 slide 15 上 65k 与 32k 两个数值也是页内读数,引用时按页内表处理。
第三段的两条上限写在讲义 slide 16。每帧先算每个头发模型的屏幕面积,据此把发丝数缩放,维持整屏「每 64×64 贴片不超过 2000 段」这条上界,这条上界在所有平台一致,讲者说这个值在他们自己的内容上给出了不错的画质与性能平衡。算出各模型初始 LOD 之后,再看所有在渲染模型的段数合计,把它压到 200 万段以内;超了就按比例减每个模型的发丝,直到放得下。这条合计上限顺带给所有逐顶点计算与内存定了上界。针对远处的模型还有一招:按距离动态地隔一个顶点跳一个,把被着色的顶点数减半。讲者把它的边界说得清楚,这一招只帮着色成本,对光栅化没有帮助,因为光栅化按段走;它也不省内存,只是在着色器里跳过顶点。
效果用前后两张画面说明。不启用 LOD 时,画面里只容得下 7 个头发模型,讲者补了一句其中两个还是 Indy 的手,后面其他角色什么都分不到,而这在一款会走进拥挤敌营的游戏里是个问题(讲义 slide 17)。启用 LOD 之后,所有角色都能容下,画质保持得住,而且渲染的段数还是一样(讲义 slide 18)。讲者强调两张画面用的段数相同,多出来的只是把同一笔段数重新分配给了更多模型。
本章可核对的结论有四条。LOD 靠随机化发丝顺序后截取一段并按比例加粗来保持外形,出处是 [Taillandier20](讲义 slide 14)。段数被三层上限钉住:载入时封单资产发丝数为 65k 或 32k(讲义 slide 15),每帧按屏幕面积维持每 64×64 贴片不超过 2000 段,再把全模型合计压到 200 万段,超了按比例减发丝(讲义 slide 16)。隔点取值只减小被着色的顶点数,对光栅化与内存都没有帮助。两侧的对照读数是不启用 LOD 只能放下 7 个模型、启用后能放下全部角色且段数不变(讲义 slide 17、18);讲义 slide 13 的 8.4 加 2.6 毫秒属于项目早期,与讲义 slide 12 的计时表不是同一时期。
4模拟与蒙皮:为后面所有 pass 备好数据
上一章把发丝数与段数钉死,这一章看数据怎么为后面所有 pass 备好。先要交代一条口径。讲者在开篇就说明这次不讲头发模拟的求解器与头发路径追踪(讲义 slide 1)。本章要处理的模拟与蒙皮,只写到与渲染管线耦合的那一层:求解器的出处、蒙皮的数据结构、缓存的读数。求解器内部怎么迭代、约束怎么解,讲义没有展开,这里也不替讲者补,本章只交代这条边界。
模拟这一层写在讲义 slide 19。求解器是 PBD(位置基动力学)的不可拉伸求解器,基于 [Müller12],每根发丝、每根发丝上的每个顶点都参与模拟。模拟在头部蒙皮之前跑,所以它只考虑主头关节的运动,发丝蒙到单个头关节上,没有混合形状(blend shapes)。头部碰撞用预先烘好的头部 SDF,每颗头约 0.25 MB。头发对身体的碰撞没有做,讲者给的理由很像这一章的口气:本作的发型都不长。模拟跑完输出局部空间的顶点位置与速度,供后面的 pass 使用;它还输出密度体素网格,既用于头发碰撞,也用于头发影子的计算,密度体素网格因此是碰撞与影子两层共用的输入。模拟逐资产可选,由美术开启,但当头发需要对皮肤投影子时,它是必选。
蒙皮这一段的任务是把发丝钉在基础网格的三角形上(讲义 slide 20)。资产导入时,团队为每根发丝根算出父网格上最近的三角形、它的重心坐标与切线框架,存进 hairRootSkinningInfo_t:父三角形索引、压位的重心坐标(x 与 y 打包,z 重建)、八面体编码的三角形法线、八面体编码的三角形切线。每根发丝根 80 字节,其中每个网格 LOD 16 字节、共 5 档 LOD,所以这份数据要按父网格的每一档 LOD 各存一份,父网格有多档 LOD、各档的三角形索引并不相同。运行时的做法是先把头部网格蒙好,再直接取变换后的三角形来变换发丝。这套东西对主发型通常不需要,对动物、面部毛发与手部绒毛则是绝对必需,因为它们的父网格会动。
变换后的结果在除以 w 之前被存进一份全局缓存(讲义 slide 21)。这份缓存要吃显存:200 万顶点占 32 MB,低配为 100 万顶点。Series S 上平均 0.17 毫秒,讲者说这一段受内存带宽约束。之所以舍得,是因为同一份数据被多个 pass 复用,光栅化、着色与合成都会取它,合成阶段还要用它来算 motion vector(运动矢量)与插值系数。这份缓存是本章与后面所有 pass 之间的接口,光栅化按段取顶点、着色按段取数据、合成按像素取位置。这份缓存里同时装着蒙好的父网格三角形、局部空间的模拟发丝顶点与裁剪空间的发丝顶点三类数据。
本章可核对的结论有三条。模拟基于 [Müller12] 的 PBD 不可拉伸求解器,逐发丝逐顶点,跑在头部蒙皮之前,因此只考虑主头关节的运动,输出顶点位置、速度与密度体素网格(讲义 slide 19);头部碰撞用每颗头约 0.25 MB 的预烘 SDF,头发对身体几何的碰撞没有做。蒙皮把发丝直接钉到基础网格的三角形上,每根发丝根存 80 字节,按 5 档父网格 LOD 各存一份,运行时先蒙头部网格再取变换后的三角形(讲义 slide 20)。变换后的顶点存进全局缓存,200 万顶点占 32 MB,Series S 上平均 0.17 毫秒,受内存带宽约束,这份缓存被后面的 pass 与合成复用(讲义 slide 21)。
5剔除与软件光栅器的选择
第 4 章把变换后的发丝顶点与密度体素网格备齐,这一章从这批顶点往下走,先划掉不必画的段,再决定用什么把剩下的段画进缓冲。讲义 slide 22 把这一步称为粗剔除,产出物是一份要光栅化的段列表,丢掉不可见的段。这一页列了三件事,视锥剔除、遮挡剔除,以及为帽子单独做的一项裁剪。讲者把三类并列表述,前两类是通用做法,第三类专为帽子的道具属性而设。
遮挡剔除把每一段对着降采样的深度缓冲测试。讲者在此处给了一句限定,如果一个镜头是近景、每一段占屏比例很大,可能要建完整的 Hi-Z 链;在本作的镜头下,只用各轴 1/16 降采样的深度缓冲就够(讲义 slide 22)。第三件叫费多拉裁剪。帽子是印地安纳琼斯的标志性道具,游戏里戴帽脱帽的场景很多,需要防止头发穿出帽子。讲者说他们试过不同做法,最后落在两个形状上:一个排除盒,把帽子以上的段全部裁掉;一个包含球,用来容纳帽子下方那块区域。页面把这两个形状并列为两条裁剪形状,两者配合算出可见屏幕包围盒。
讲义 slide 23 是这一页的续行,讲的就是这个包围盒的去处,用它算出模型需要在可见性图集里占多少空间。这一页只有一行文字,是从上一页接过来的半句话,也说明剔除的产出不止一份段列表,还包括每个模型在图集里要占的位置。
选完要画的段,接下来要选画法。讲义 slide 24 给了三条理由。硬件光栅在待画图元很小时性能不佳,而发丝段常常细于一个像素;团队想把尽量多的工作挪到 async compute 上,软件光栅才方便这样安排,因此他们没有硬件光栅的版本;软件光栅还更容易画出完美的抗锯齿细线。三条合在一起,促成了他们自写一个保守光栅器(conservative rasterizer),针对短线段优化,灵感来自 [Taillandier20]。它用原子操作写进可见性缓冲(visibility buffer),之后与其他透明一起 resolve。
讲者没有停在选型上,讲义 slide 25 把朴素实现摊开,也把它的代价说明白。每个着色器 lane 分到一段,把这段当一个梯形来光栅化,用一个二维循环逐列逐行扫过像素;每个像素算覆盖率,与全精度深度缓冲做深度测试,通过就原子写进可见性缓冲。这套算法简单,代价却很高:依赖型读取很多,GPU 缓存利用远不理想,lane 分歧(lane divergence)也高。页内注释还给两处操作定了价,深度测试是第二贵的部分,写进可见性缓冲是单项最贵。讲者用这一页把下一步的问题定死,多层的可见性缓冲该怎么写、写下去要花多少内存,是整套系统里最该先动的地方。
本章可核对的结论有三条。剔除一侧做三件事,视锥剔除、对 1/16 降采样深度的遮挡剔除,以及用排除盒加包含球两把形状做的费多拉裁剪(讲义 slide 22),两个形状算出的屏幕包围盒再用来定图集占用(讲义 slide 23)。选软件光栅的理由有三条,硬件光栅对细于像素的图元性能不佳、要把工作尽量塞进 async compute、以及更容易做完美抗锯齿的细线(讲义 slide 24)。朴素实现在讲义 slide 25 上自陈很慢,最贵的一项是写多层可见性缓冲,这也是下一章要动手的地方。
6可见性缓冲:压到 32 位载荷与覆盖率重建
第 5 章把最贵的一项指为写多层可见性缓冲,这一章就把这块缓冲拆开改。讲义 slide 26 先给结构,每个像素有 3 个按深度排序的层,用 3 次 atomicMinOut 填(做法类似 [Ishihara23]),另加一层累加覆盖率。讲者当场算账,每像素 3 个 64 位值再加 32 位累加覆盖率,1080p 光可见性缓冲就要 56 MB。64 位原子在性能上还过得去,但他们要换用 32 位载荷,既压低内存,也给后面的优化腾出前提。页内还点了第二笔代价,这段代码里依赖型原子操作很多,本身就慢。
改 32 位要先解决装不下这件事,讲义 slide 27 把载荷切成一宽一窄两段。段索引给 22 位,代价是单个模型的段数被钉在这个范围内;讲者觉得可以接受,反正他们不想一屏渲染太多段。剩下 10 位给深度,够排序,远不够表示场景深度,于是深度在模型包围盒内归一化并线性化,10 位就够用;副作用是偶发 z-fighting,基本看不出来。还有一处连带后果,载荷里再没有位置放模型索引,于是一个可见性缓冲区域只能由一个模型写。
把整屏缓冲换成贴片图集是讲义 slide 28。图集由 64×64 像素的贴片组成,按可见段的屏幕包围盒来分配,全被剔除的模型跳过分配,每个发型模型各自分配。一块贴片只装一个模型,这正好接上上一页载荷里放不下模型索引的限制。用图集的另一处好处是,花在头发图集上的内存与屏幕分辨率脱钩。
讲义 slide 29 处理图集本身也会不够用的情况。图集有硬上限,200 万像素、32 MB;重叠的发型可能要的贴片比图集能给的更多。检测到这种情况就逐贴片降分辨率,直到所有贴片放得下。这顺带给出一个好处,图集的像素数固定,光栅化要处理的像素数也就有了上界。为了避免降分辨率带来的可见像素化,合成时按泊松盘(Poisson disk)分布做随机采样。实测里只用到 x2;讲义 slide 30 给出 x2 与 x4 的对照,讲者说理论上能塞下几乎无限的头发,实际从没走到半分辨率之外,x4 一档从未用上。
覆盖率那一层在讲义 slide 31 说明来由。3 个前层不足以得到不透明的头发,典型发丝只盖住像素约 20–25% 的面积。高质量的顺序无关透明(OIT)太贵,也让渲染复杂不少;他们要的是一个能合并像素里几十个片段的快速近似,于是用 atomicAdd 累加覆盖率,合成阶段再重建 alpha 混合后的覆盖率。
讲义 slide 32 给重建函数。两条假设先立好,同一像素上所有发丝的屏幕厚度相近,BSDF 也相近。在这两条下,他们数值模拟总覆盖率随累加覆盖率的变化,做法是画一堆不透明线,每画一条算一次精确总覆盖率,再用多项式去拟合结果。讲者说五次多项式拟合得很好,但发货用了二次,因为它略便宜,且视觉上更不透、更有体积感。页内的取用代码是先令 x = additiveCoverage / 1024.0f,再以 saturate( x - 0.5f * x * saturate( 0.5f * x ) ) 得到总覆盖率。讲义 slide 33 把五次与二次两档并排,视觉上很接近。
讲义 slide 34 收掉这一章,也给出一处讲者自陈的灰色地带。做法是对可见性缓冲的第三层提前剔除,用非原子 load 去读那一层,让 GPU 缓存能起作用。讲者把这处明说是规格的灰色地带,因为同一个内存位置被混用了原子与非原子访问;就本作的情况而言,他们没有发现任何问题。即便提前剔除掉了,累加覆盖率仍然要照写。对密集的发型,这一处能省下相当可观的时间,页内给的量级是最多省一半光栅化时间。
本章可核对的结论有四条。缓冲是每像素 3 个有序层加一层累加覆盖率,用 3 次 atomicMinOut 填层,每层 64 位时 1080p 要 56 MB(讲义 slide 26)。载荷压到 32 位后是 22 位段索引加 10 位深度,深度在模型包围盒内归一化并线性化,代价是偶发 z-fighting 与一个区域只能由一个模型写(讲义 slide 27)。整屏缓冲换成 64×64 贴片图集,上限 200 万像素、32 MB,不够就逐贴片降分辨率,实测只用到 x2(讲义 slide 28 到 slide 30)。覆盖率靠 atomicAdd 累加、合成时用二次多项式重建;对第三层提前剔除的提速被讲者自陈为规格的灰色地带(讲义 slide 31 到 slide 34)。
7把分歧压下去
第 6 章把可见性缓冲的内存与载荷压下来,这一章处理它的另一半代价,lane 分歧(lane divergence)。讲义 slide 35 先给现状,平均只有 45% 的 lane 在干活。分歧的来源是发丝段的厚度、长度与斜率各不相同,而两层循环的边界也跟着各不相同,同一个 wave(lane 组)里因此塞进了差别很大的段。讲者给了一段直观描述,光栅化先自上而下、再自左向右,同一列里只要还有一个 lane 有活像素,整个 wave 都得等它,页内把这些等待画成橙色的假像素,并用三组 lane 并排示意,一条有活、另外两条只能等。他还补了两句,wave 内的指令是锁步执行的,某个 lane 本可以走到下一列,却仍要停下等邻居。
第一处改造在讲义 slide 36,把两层循环压成一层。内层循环变成一个条件语句,到底了就换到下一列,于是不同 lane 可以同时光栅化不同的列,厚度与斜率带来的分歧随之消失。剩下的分歧只剩一个来源,各段要光栅化的总像素数不同。讲义 slide 37 把剩下的分歧逐条列出:像素少的 lane 要等像素多的;覆盖率为零的要等有覆盖率的;深度测试没过的要等过了的。这些操作还互相依赖,延迟大,停顿难以掩盖,页内注释把深度测试标成长停顿,把写缓冲标成更长的停顿。
第二处改造在讲义 slide 38,叫像素请求栈。做法是把像素写入请求先攒进 LDS,等凑满一个 wave 的量再批量执行。这样攒是发散但便宜的本地操作,写是在统一控制流下的昂贵操作。讲者说他们后来把同一套机制也用到了深度测试上,发货时有两块栈,一块给深度测试,一块给真正的像素写入。他还给了一句劝告,栈可以再加,但每多一块栈就多占一份 LDS,他只建议给最贵的发散操作加。这样分工的收益在空闲 lane 上最明显,手上暂时没活的 lane 在每次迭代里跳过光栅化工作,轮到批量写出时照样参与全局内存操作,贵操作的并行度因此被拉满。
讲义 slide 39 交代栈的细节。每个 wave 在工作组里独占一段 LDS 空间,各块内存不重叠,所以只需要 wave 级屏障,用不着 group 级的 LDS 屏障。每个 wave 预留 GROUP_SIZE * 2 条请求以防溢出。页内把要存的东西也列了,可见性缓冲的载荷、像素位置、精确覆盖率,还有第二块栈的载荷与图集偏移。讲者提醒,LDS 用量不小,把请求载荷尽量压小是控制 LDS 占用与占用率的关键。讲义 slide 40 用一个 wave 只有 4 个 lane 的假想硬件演示推与弹,前两次迭代只攒请求、不动全局内存,凑够一个 wave 的量才统一写出,写完之后继续攒。
结果在讲义 slide 41 一页给全。循环扁平化把分歧从 55% 降到 45%,像素请求栈再降到 20%,平均活跃 lane 于是从不到一半升到约 80%,页内另标 20% 的分歧。讲者说所有重操作都跑满了 lane,着色器快了近一倍。两处改造的分工也清楚,扁平化针对的是厚度与斜率带来的边界差异,请求栈针对的是像素数、覆盖率与深度测试这三处小分歧。
本章可核对的结论有三条。初始状态是平均只有 45% 的 lane 活跃,分歧来自厚度、长度、斜率各异的段被塞进同一个 wave,同列里只要还有活像素,整个 wave 都要等(讲义 slide 35)。两步改造各自对应一处来源,循环扁平化去掉厚度与斜率分歧、把分歧降到 45%,像素请求栈再把分歧降到 20%(讲义 slide 36 到 slide 40)。两处的合成效果是活跃 lane 升到约 80%、着色器快近一倍(讲义 slide 41)。
8让缓存用得上:全局排序
第 7 章把分歧压到 20%,讲者说这时的工作已经高度并行,下一个问题浮上来,缓存命中率低。讲义 slide 42 给出原因,发丝顺序在资源里被随机化过,那是为简化 LOD(细节层次)而做的;代价是每个 lane 去访问可见性图集里相隔很远的位置,像素写入没有空间连贯性。页内把这一页的两行判词并排写着,分歧已经压得低,缓存命中率却仍然低;讲者用一屏 4000 个连续段做例子,说它们散布得到处都是,并把话说明,要拿回缓存,就得让发丝段有空间连贯性。页内列的三行原因连成一条链,发丝被随机化、目的是简化 LOD、结果是像素写入失去空间连贯性。
解法是全局排序,讲义 slide 43。讲者的定位很宽松,不需要完美排序,只要同一个 workgroup 里的段在空间上彼此靠近就行。做法是计数排序(counting sort),把屏幕划成桶,桶宽约等于一块可见性图集贴片;在剔除 pass 里统计桶计数,对桶做一次前缀和,再写出部分有序的段索引。桶的尺寸与图集贴片接近,落在同一桶里的段在图集里也就挨得近。讲者说它很快,在多数硬件上不到 100 微秒;页内并排给出 4000 个连续段排序前后的样子。
讲义 slide 44 再往前一步。这一步的排序粒度还不够细,他们发现在光栅化着色器内部、每个 workgroup 里按莫顿曲线(Morton curve)再排一次,也对性能有正面影响,只是幅度较小。它复用在用的像素栈那块 LDS,用的是同一套计数排序,只是范围缩到一个 workgroup 内。代价是需要大的 group,256、512 或 1024 个 lane,且最优的 group 尺寸因硬件而异,讲者要听众自己去测。他也把这一步的分量说清,它复用的是现成设施,省得不多,并且换一块硬件结论就可能变。
讲义 slide 45 处理一个反向需求。后面还有发丝空间着色(strand-space shading),它要取大量逐段数据,此时需要的是自然顺序;若沿用空间顺序,着色阶段的缓存命中率又会掉下来。这里的做法是再排一次、把顺序还原成自然顺序;这次不需要桶,可以直接利用 GPU 发射 wave 的顺序。页内给了一段用 subgroup 操作的写法,先用 subgroupAdd 数出本 wave 里有多少可见段,再用 subgroupExclusiveAdd 求各自的偏移,由一个被 subgroupElect 选中的 lane 用原子操作在全局变量上取号,最后把段索引写到排好的位置。另一种做法是存两份列表,一份是按空间排好的可见段,一份是不排序的可见顶点,代价是更多内存。两次排序的方向相反,一次服务于光栅化写入的缓存,把段按屏幕位置聚拢;一次服务于着色取段时的缓存,把段还原成自然顺序。讲者在这一页同样提醒,不同硬件表现不同,务必自己测。
本章可核对的结论有三条。缓存命中率低的根因是为 LOD 做的发丝随机化让访问失去空间连贯性,一屏 4000 个连续段彼此散开(讲义 slide 42)。解法是全局计数排序,按约等于图集贴片的桶在剔除 pass 里统计并做前缀和,不要求完美排序,只要求同组段彼此靠近,多数硬件上不到 100 微秒(讲义 slide 43)。在组内还可以按莫顿曲线复用像素栈 LDS 再排一次,收益较小、需要大 group 且最优尺寸因硬件而异(讲义 slide 44);着色阶段则要还原成自然顺序,靠 GPU 发射 wave 的顺序或存两份列表(讲义 slide 45)。
9自阴影:三条被否的路与最终那条
上一章先把可见段排成空间顺序让光栅化吃缓存,再排回自然顺序供着色取数(讲义 slide 43、45)。这一章进入着色段(讲义 slide 46)。着色按顶点做,讲者给的理由是逐顶点结果更平滑,也更省开销。散射沿用 [Marschner03] 的单根发丝模型,不含 glints;三个镜面瓣里,不染色的反射(R)与第二次染色反射(TRT)的近似取 [Karis16],逆光时可见的透射瓣(TT)取 [Tafuri19],另有一个多重散射近似。每一瓣再挂一个非 PBR 缩放,供美术做临时微调。讲者明说这回不细讲模型本身,重点放在自阴影上。
Marschner model(Marschner 散射模型)对人发效果很好,可 R 与 TT 两瓣在多数入射方向上都偏亮,缺了 self-shadowing(自阴影)画面就会明显不对(讲义 slide 47)。麻烦有两头。游戏会用不投影的灯来省性能;就算只用投影灯,也得把影响头发的灯数压到最低,而过场的光照配置可以很复杂。讲者还堵掉一条捷径,给不投影的灯关掉透射瓣,效果依然不理想,因为反射瓣本身就带着大量能量。需要一种在全部灯上都成立、又不随灯数膨胀的做法,于是有了三条候选路。
第一条是 deep opacity map(深透明度图)[Yuksel08]。画质好,代价是每个灯都要把头发再光栅化一遍,还要额外一批多通道、高分辨率的贴图。主视图的光栅化本来就占去预算一大块,再为每个灯重画一遍不现实,这一条因内存与重复光栅化被否(讲义 slide 48)。
第二条改沿光方向对密度体走 raymarching(类似 [Jansson19])。它不占额外内存,可直接复用模拟产出的密度体,也不需要额外光栅化,分辨率与步数都能调。代价随之而来,每个着色点都要朝每盏灯走一遍体,开销随灯数线性增长。精度也是问题,部分 groom(发型资产)需要每轴 256 体素才能把阴影解干净,否则留下自身遮挡或漏光的伪影,而这条整体拿不到高频细节(讲义 slide 49、50)。
第三条是随机方向追踪加球面探针,讲者把它归在 [Gerard23] 与 [Ciardi22] 一类。做法上不再沿灯方向走,改成随机打固定数量的射线,求出一个球面可见性函数,再用 spherical harmonics(SH,球谐)或球面高斯编码。它换来两项解耦,与灯数无关,一次查表就能解出可见性;与着色点也解耦,讲者试过 1 个探针配 4 个着色点,结果与 1 配 1 很接近,还可以跨帧分摊。问题出在成本上,这条路仍然太贵,在 Series S 上单个发型就要约 6 毫秒,而且网格分辨率得开得很高,否则头发会显得太软(讲义 slide 51)。这个 6 毫秒绑定 Series S、绑定单个发型,并且这条方案最终没有发货。讲义 slide 52 给出这一路的结果,光照可信,但整体偏平。
三条都被否,讲者说这几趟尝试给了他两条经验。本作大多数发型本来就偏静止、偏短,幻灯片给的是游戏里最长的那款;对自阴影而言,真正起作用的是每段发丝附近的近邻,而这份近邻在风下变化不大。有了这两条前提,可见性可以只烘一次、反复多用。他们最终发货的做法是把烘制放到离线,用软件光追给每个发丝顶点追可见性射线,求出球面函数并烘下来,运行时只做一次查表,对全部灯生效,代价基本免费,且与灯数无关。追射线时还把父网格也就是头部一起追,于是头发总会被头挡住(讲义 slide 53)。
本章可核对的结论有三条。头发按顶点着色,散射模型是 [Marschner03] 的三瓣结构,R 与 TRT 瓣的近似取自 [Karis16]、TT 瓣取自 [Tafuri19],每瓣另加非 PBR 缩放(讲义 slide 46、47)。三条候选都被否,深透明度图 [Yuksel08] 倒在内存与每灯重光栅化上,沿光 raymarch 密度体 [Jansson19] 缺高频且部分发型要每轴 256 体素,随机方向追踪加球面探针 [Gerard23][Ciardi22] 在 Series S 上单个发型要约 6 毫秒且未发货(讲义 slide 48、49、51)。发货的是离线预计算软件光追,运行时查表对全部灯生效,并连带对头部追踪(讲义 slide 53)。下一章看这份球面函数编的是什么,以及它在哪个角度会漏光。
10可见性函数与它的坑
上一章把可见性挪到离线烘焙,这一章讲这份烘下来的结果到底编了什么,以及它在哪个角度会漏光。开头先用几页画面把烘焙可见性兑一遍。讲者原本担心它在运动下会崩,实测站得很稳,强风下能看出一些伪影,按他自陈仍在可接受范围内(讲义 slide 54)。单灯加调试视图那页上,烘出来的可见性把高频细节表现得很清楚(讲义 slide 55)。这一页把单光源下的 Gina 分成调试光照、发丝与可见性三栏,色标从 100% 可见到 0% 可见。后面几页是把体积 raymarching 与预计算光追逐组对照,预计算的结果不那么平,耳朵的阴影位置对得上,自身遮挡不再成问题(讲义 slide 56 至 59)。这几页的备注各有侧重,讲义 slide 56 与 slide 57 都点出耳朵投下的影子位置正确,slide 58 换成别的发型再比一次,slide 59 说明不去用体素网格之后自身遮挡就不再冒出来。这几页只有画面与备注,没有对应插图。
函数定义在讲义 slide 60。他们用受限的低阶 spherical harmonics(SH,球谐),这里取 L1,选它的理由有三条,积分简单、内存小、不会出现振铃。低阶球谐抓不住高频角度信息,所以他们没有直接去编可见性,改去编任意方向上的平均散射事件数。即便这样处理,直接用球谐仍会丢掉高频角度细节,幻灯片右侧的极坐标图把这件事画得很清楚,后果是漏光,以及掠射角上的能量损失。页内把目标发丝、近邻发丝与头部几何三者并列,作为这套追踪面对的三类对象,极坐标图用来对比直接编可见性与改编事件数两种口径。
修正放在讲义 slide 61,也是这一章的关键。他们给没有命中任何东西的射线加一个负的散射事件数偏置,发货取值是 HAIR_MAX_SCATTER_EVENTS = 5,把未命中的事件数取成负值;取用时先做 max(0, ·),再按吸收求可见性。页内两段伪代码把烘制与取用分开写,烘制段在 numScatterEvents == 0 时写入 zeroEventBias = −HAIR_MAX_SCATTER_EVENTS,取用段对投影结果取 max,免得负值漏到最后一步。这条偏置的作用是避免掠射角上的过度遮蔽,让过渡更锐,否则逆光下的头发会丢掉大部分「透光感」(讲义 slide 61)。
HAIR_MAX_SCATTER_EVENTS = 5 这个发货取值。②读法(怎么读):把两段代码分开读,烘制段给未命中射线赋负偏置,取用段先 max(0, ·) 再求可见性;这一页是第 9 章离线烘焙与第 11 章影子计算之间的桥。
来源:SIGGRAPH 2025 · Advances in Real-Time Rendering in Games(Part I)· Strand-based hair and fur in "Indiana Jones and the Great Circle" · 讲者 Sergei Kulikov(MachineGames)· 未修改引用
加偏置前后的实际画面对照在下一页,讲者的判断是加偏置后画面更接近正确,轮廓处的逆光不再丢光(讲义 slide 62)。讲义 slide 63 给出的是未来工作,他想试其他球面函数,可能包括学出来的函数,并做可见性的动态重算,以更好处理强风与长发;这两条按未来工作引用,目前没有落地。
这份可见性还有一个与 LOD(细节层次)天生冲突的地方,讲义 slide 64 把它挑明。烘可见性的时候所有发丝都在场,而 LOD 会把其中一部分发丝减掉,被减掉的这部分在烘好的数据里仍然遮挡着还看得见的发丝,讲者把这种残留叫作「幽灵」。理想的做法是为每一档 LOD 重烘一份,可 LOD 的档数等于发丝数,内存不允许。他因此说,经典定步长的 LOD 也许反倒是更稳的选择。讲义 slide 65 用减幅 90% 做了对照,一边用全部发丝烘、一边只用可见发丝烘,实际伪影并不明显,于是他们保留了原来的 LOD 系统。
本章可核对的结论有四条。烘出来的可见性在角色运动下站得住,强风下有伪影但可接受,单灯调试视图显示高频细节保留良好(讲义 slide 54、55)。可见性用受限的 L1 球谐编码,编的对象是任意方向上的平均散射事件数;直接编球谐会丢高频角度细节,导致漏光与能量损失(讲义 slide 60)。修正办法是给未命中射线加负偏置,发货取值 HAIR_MAX_SCATTER_EVENTS = 5,取用时先取 max(0, ·)(讲义 slide 61),加偏置后逆光轮廓不再丢光(讲义 slide 62)。预计算可见性与 LOD 的冲突至今未解,被 LOD 减掉的发丝仍以「幽灵」形式遮挡,理想解要按每档 LOD 重烘而内存不允许(讲义 slide 64、65)。下一章从像素一侧把这份可见性变成别的物体上的影子,并收尾合成与覆盖率重建。
11影子、合成与覆盖率重建
上一章把可见性函数编好,也留下它与 LOD 未解的冲突。这一章从像素一侧把这套数据用起来,先算头发投在别的物体上的影子,再讲合成与排序,最后收在覆盖率重建与运动矢量,顺序按讲者的页序走(讲义 slide 66 至 83)。
头发对其他物体的影子用体积 raymarching。对皮肤像素,沿光方向走一遍头发密度体,每盏灯 6 步、每像素 1 条射线,网格是 48×48×48;同一套做法顺带拿到环境遮蔽,用 1 条 2 步的射线。美术可以按资产控制阴影强度。过场里在 Series S 上,环境遮蔽与直接阴影合计约 0.3 毫秒,讲者说只对不透明像素做这件事时速度够用(讲义 slide 66)。
这套做法有利也有弊,讲义 slide 67 用两栏把它们分开写。好处是不需要光栅化、开销随屏幕分辨率伸缩、可以按需启用、散射观感自然。坏处有两处必须记住,密度体来自模拟,所以这套做法只支持蒙到单个头关节,动物会弯折,动物毛因此用不了;另外它做不了头发投在墙与地板上的影子。讲者解释为何没有继续补,他不想再为头发体积求交去搭一套加速结构。讲义 slide 68 交代了墙地板缺影子的代价,平时不显眼,可放到玩家角色身上就会破轮廓。讲义 slide 69 给出解法,给玩家角色做一把简单的、雕刻出来的 geometry proxy(影子代理网格),把面翻过来以免皮肤上出现双重阴影,再画进阴影贴图;它渲染很快,美术也好做。
合成一侧,讲义 slide 70 给出结构,按背到前排序,每个发型画一个面向相机的四边形,与其他混合几何(粒子、玻璃)放在一起绘制,输出光照、深度与运动矢量。因为每个模型在可见性图集里有自己的区域,必须逐模型单独合成,这样也能把复杂的透明叠放统一处理。排序靠深度的启发式加偏置(讲义 slide 71),面部毛发永远先于主发型绘制,其余头发整体带约 0.5 米偏置,好让近距离的软混合烟粒子总在头发之后;他们边画边写深度,粒子的软混合才正确。多数场景开箱可用,个别复杂场景仍需手工调偏置。讲义 slide 72 至 79 是同一个复杂排序镜头的逐帧示例,讲者给的顺序是先烟在飞机之后、角色身后的玻璃、Gina 的头发、主角的睫毛与眉毛、主发型、前玻璃,玻璃那层要调偏置并把前后两面分开画。
覆盖率重建是合成里最实的一块。一个像素里只留了最靠前的 3 个段,可要重建的是这个像素上全部片段的混合光照(讲义 slide 80)。做法是把缺失部分建模成前 3 个样本的加权平均。前 3 个样本按常规透明度混合给出 C_front = 1 − (1−C₁)(1−C₂)(1−C₃),总数 C_total 取自可见性图集的累加覆盖率,两者之差 C_missing = C_total − C_front 就是要补的覆盖;缺失部分的光照 L_missing = C_missing · ΣwᵢLᵢ / Σwᵢ,权重 wᵢ = Cᵢ · 0.25^(3−i),越靠近相机的样本权重越小,也就是把重建的偏压放向离相机更远的样本,因为它们更接近要被重建的那部分,最后 L_total = L_front + L_missing(讲义 slide 81)。
Cᵢ · 0.25^(3−i) 取。②读法(怎么读):先把 C_total 与 C_front 两者的来源分开,前者来自可见性图集的累加覆盖率、后者只由 3 个前样本算出;再读权重那一项,它决定缺失光照偏向哪一侧。
来源:SIGGRAPH 2025 · Advances in Real-Time Rendering in Games(Part I)· Strand-based hair and fur in "Indiana Jones and the Great Circle" · 讲者 Sergei Kulikov(MachineGames)· 未修改引用
讲者用一页对照说明这套近似的分寸(讲义 slide 82),近处靠 3 个前样本已经够,远处则靠重建补上更多光照信息。最后一项是 motion vector(运动矢量),讲义 slide 83 把它接回第 4 章那份全局变换后缓存,把当前帧变换后的 NDC 压位,留给下一帧当上一帧位置用;若某顶点上一帧没有被变换,比如 LOD 变化,就按没动处理;x 与 y 用 clamped 的 UNORM16,讲者说对保守光栅(conservative rasterizer)的精度够。他的建议是,如果光栅化不保守、要靠 TAA 重建头发,并且内存上还有余量,就改用全精度,画面清晰度会明显更好。
本章可核对的结论有五条。头发对皮肤的影子里,密度体体积 raymarching 每灯 6 步、每像素 1 条射线、48³ 网格,顺带做环境遮蔽,Series S 过场约 0.3 毫秒(讲义 slide 66)。两条例外必须带上,密度体来自模拟、只支持蒙到单个头关节,动物毛用不了;头发对墙与地板做不出影子,玩家角色靠一把雕刻影子代理网格补上,面翻过来避免皮肤双重阴影(讲义 slide 67、68、69)。合成是每个发型一个面向相机的四边形,与粒子、玻璃一起背到前绘制(讲义 slide 70),排序靠带偏置的启发式,面部毛发永远先画、头发整体约 0.5 米偏置(讲义 slide 71)。覆盖率重建用前 3 个样本的混合覆盖率与图集总覆盖率之差,按偏向远处的权重把缺失光照补出来,wᵢ = Cᵢ · 0.25^(3−i),L_total = L_front + L_missing(讲义 slide 81)。运动矢量复用上一帧的变换后 NDC,LOD 变化时按没动处理,x、y 用 clamped 的 UNORM16(讲义 slide 83)。下一章收内存账、五条结论与全部边界。
12内存、结论与边界
上一章的合成把头发画到屏上并写出运动矢量,这一章收全篇的账。内存一页的口径绑在 Series S 与 PC 低配上(讲义 slide 84):与内容无关的分配是 131 MB,其中模拟数据 69 MB、渲染数据 62 MB。渲染侧的明细列出可见性图集 32、顶点变换后缓存 16、着色点 4、上一帧位置 4、可见段索引 4、排序桶 2,这几项相加正好是 62,与总数自洽(讲义 slide 84,页内表;该表的行列对应关系未经人工核对)。内容相关的部分,单个 LOD0 发型资产约 10 至 30 MB,高配允许两倍发丝,渲染数据随之从 62 MB 升到 90 MB。讲者自陈没有实现几何流送,时间不够,所以依赖资源的显存占用随场景而变;未来工作里他想减少模拟需求并补上资产流送。
结论五条在讲义 slide 85。发丝在当代硬件上够跑 60 FPS,可以当作头发的主方案;要挑准战场,做不到全都要;给每一部分都设好最坏情形上界,是可扩展的关键,否则内容迟早会突破所有限制;软件光栅化是发丝的正确选择,把它做快需要花不少时间,但回报明显;动态头发还需要更多优化,眼下性能与更好的头发运动只能二选一。讲者对这五条各给了一句支撑,第一条被他当作主要结论,理由是发丝如今已经快到足以当主方案;第二条要求先认清目标再决定取舍;第三条说给系统的每一部分都定好一个最坏情形能省掉大量返工,否则内容最终会突破所有限制;第四条承认软件光栅要投入不少时间才跑得快,换来的结果划算;第五条指出动态头发还欠优化,眼下的选择只有性能或更好的运动。讲义 slide 86 是参考文献一页,列出 [Taillandier20]、[Müller12]、[Ishihara23]、[Marschner03]、[Karis16]、[Tafuri19]、[Yuksel08]、[Jansson19]、[Gerard23] 等,其中几条各自对应前文的一处方法出处,例如 [Müller12] 是模拟求解器、[Taillandier20] 是随机化 LOD 与软件光栅的灵感来源、[Ishihara23] 是可见性缓冲的多层写法;这些都属于学术出处,不是代码许可。
把这几页合成一句话:真正把发丝撑进 60 FPS 的,是给每一段工作都设上界,从发丝数到段数、从图集像素到内存与灯数。下面把全篇需要带着口径读的地方集中列成八条边界。
边界 1: 讲义 slide 12 的计时表是「关闭 async compute」的口径,关闭时 Series S 合计 2.39 毫秒、已超 2 毫秒预算,开启 async 后平均补回约 0.5 毫秒才落进预估;讲义 slide 13 的 8.4 加 2.6 毫秒是项目早期读数,两者不是同一时期,不可并列成前后对比。
边界 2: 内存账绑定 Series S 与 PC 低配,与内容无关的 131 MB(模拟 69 加渲染 62)出自该配置;高配允许两倍发丝,渲染数据为 90 MB;单个资产 10 至 30 MB 的口径是单个 LOD0 发型。
边界 3: 三条候选自阴影方案都是被否的,分别是深透明度图 [Yuksel08]、沿光 raymarch 密度体 [Jansson19]、随机方向追踪加球面探针 [Gerard23][Ciardi22],发货的是离线预计算软件光追;讲义 slide 51 的 6 毫秒是 Series S 上单个发型的读数,且该方案未发货。
边界 4: 讲义 slide 34 的「非原子 load 混用原子写」是讲者自陈的规格灰色地带,本作未发现问题,但按提速手段引用,不写成规范做法。
边界 5: 两处例外要写明,体积阴影的密度体来自模拟、只支持蒙到单个头关节,因此动物毛用不了;且头发对墙与地板的影子做不了,玩家角色靠一把雕刻的影子代理网格补上。
边界 6: 预计算可见性与 LOD 的冲突未解,烘可见性时所有发丝都在,被 LOD 减掉的那部分仍会遮挡,也就是「幽灵」;理想做法要为每档 LOD 重烘,但档数等于发丝数、内存不允许。
边界 7: 不与任何商用方案横比,对照对象只有讲义里讲者自己列的几条路与他们自己的实现;内存与计时读数绑定 Series S、PS5 等具体平台,不可外推到别的引擎或硬件。
边界 8: 页内表格的行列对应关系未经人工核对,讲义 slide 12(四主机计时)、slide 84(内存分配)等表在 PDF 里是散块文本,正文只引用能自洽对齐的部分,不确定处写「讲义未给该行的完整口径」,不得凭推断补全。
13贡献块
三条可搬走的做法与站内互读
一场「把新方案塞进旧预算」的讲座能留下什么,取决于它把哪些工程判断讲到了别人抄得走的程度。这一篇留下的三样都不在算法的新意上,而在「怎么让一个昂贵的新系统在既定帧预算里跑起来」:一套处处设上界的预算纪律、一套对 GPU 分歧与缓存的动手方法、以及一次把「与灯数无关」立成约束后的路线选择。它还有一条贯穿全篇的口径纪律,主机计时、被否方案的单点开销与内存账分列,正文凡出场即带限定,因此这三样贡献都只报「怎么判断的」,不替读者把读数外推。
贡献 1:给每一部分都设一个最坏情形上界。 发丝数在载入时封顶(65k / 32k 两档,讲义 slide 15),每帧按屏幕面积维持「每 64×64 贴片不超过 2000 段」(讲义 slide 16),全区合计压到 200 万段以内(讲义 slide 16),可见性图集给到 200 万像素就再也不涨(讲义 slide 29)。这几条上界叠起来的效果是:光栅化的像素数、被着色的顶点数、以及顶点变换缓存的显存占用,都变成可预测的量,于是「十来个角色贴脸」这种最坏情形能在预算内被提前算出来。讲者的结论把这条纪律点得很直白 —— 不给每部分设上界,内容迟早会突破所有限制(讲义 slide 85)。
贡献 2:先量分歧与缓存,再对症下药。 软件光栅器的朴素实现有两个账本:平均只有 45% 的 lane 在干活(讲义 slide 35),以及因为发丝顺序被随机化导致的空间不连贯、L2 命中率低(讲义 slide 42)。对应的两步改造都不是新算法:把两层循环压成一层消掉厚度与斜率分歧(讲义 slide 36),把昂贵的全局写入攒进 LDS、凑满一个 wave 再统一执行(讲义 slide 38),活跃 lane 升到约 80%、着色器快近一倍(讲义 slide 41);再用一次按桶的全局计数排序把空间连贯性找回来,多数硬件上不到 100 微秒(讲义 slide 43)。可搬的是这套「先量、再选最便宜的改法」的顺序。
贡献 3:把「与灯数无关」立成约束,然后用离线预计算换运行时。 自阴影试过三条路:深透明度图要每灯把头发再光栅化一遍,被否(讲义 slide 48);沿光 raymarch 密度体不占内存、但每个着色点要对每个灯走一遍体、开销随灯数线性增长,且缺高频(讲义 slide 49);随机方向追踪加球面探针与灯数解耦,但在 Series S 上单个发型就要 6 毫秒,也被否(讲义 slide 51)。最终发货的是把可见性离线烘成球面函数、运行时一次查表对所有灯生效(讲义 slide 53)——代价是与 LOD 系统冲突(烘的时候所有发丝都在,被 LOD 减掉的部分仍会遮挡,讲义 slide 64),这是他们明确记下的未解项。
站内可以接着读的几篇:篇目 11(DDGI)与本篇的自阴影探针是同一种取舍的两端 —— 前者用八面体深度图给辐照度场补可见性,本篇用 L1 球谐编「平均散射事件数」,两者的代价都落在「低阶表示抓不住高频」上,且都用某种偏置或启发式去补;篇目 32(AVBOIT)与本篇同为 SIGGRAPH 2025 Advances(Part I),都把「体素化 + 沿视线累积」当基础结构 —— 一头是深度切片上的消光累积,一头是密度体上的 raymarch,可作同批对读;篇目 30(Adaptive Tessellation)与篇目 31(SLIM)都在按预算分配分辨率或算力,与本篇「处处设上界」是同一种工程纪律。本篇讲义全篇没有给出代码仓库或开源链接,唯一的文献线索是讲义 slide 86 的参考文献清单(其中 [Taillandier20] 是 Frostbite 那一篇、[Müller12] 是模拟求解器的出处)。
14原文出处块
原文出处与引用口径
出处:SIGGRAPH 2025 · Advances in Real-Time Rendering in Games(Part I)· 讲者 Sergei Kulikov(MachineGames Sweden AB;自研引擎 MOTOR 派生自 idTech 7)· 官方讲义《Strand-based hair and fur in "Indiana Jones and the Great Circle"》,87 页,540×720 pt(竖版),来源 PDF 2026-siggraph-advances-hair-ijgc.pdf(元数据 author=Sergei Kulikov、title 为空、creator=Microsoft PowerPoint for Microsoft 365)
材料:官方讲义 87 页,其中 83 页带讲者备注(完整口播稿,合计约 3.37 万字符);幻灯区有文本层,页内标题、要点与代码片段可直接引用,正文不另标图像识别来源;但页内表格在 PDF 里是散块,行列对应关系须人工过图核;本篇事实、读数与页码取自幻灯文本与讲者备注;页码为 PDF 页序(从 0 起),引用写作「讲义 slide N」
许可:幻灯区逐页页脚为 Indiana Jones © & 2025 Lucasfilm Ltd.,并标 SIGGRAPH 2025 Advances in Real-Time Rendering in Games course;全文检索无 Creative Commons / CC BY / public domain 任何 CC 声明,亦无代码许可(无 MIT / open source / GitHub)。画面含授权的影视 IP 角色与造型(Lucasfilm),残留风险强于常规学术讲义。故 21 件图按分区非 CC 口径逐件署名(源 slide 号 + 出处 + 版权行 + 「未修改引用」),并在构建记录标注残留风险,不得写成 CC 或可自由转载;讲义 slide 86 的参考文献为学术出处,不是代码许可
引用口径:讲义 slide 12 的四主机分 pass 计时是关闭 async compute 的口径(关闭时 Series S 合计 2.39 毫秒、已超 2 毫秒预算,开 async 后平均补回约 0.5 毫秒才落进预估),slide 13 的 8.4 + 2.6 毫秒是项目早期读数,两者不可并列比较;内存账(与内容无关的 131 MB = 模拟 69 + 渲染 62,单资产 10–30 MB)绑定 Series S / PC 低配,高配渲染数据为 90 MB、单资产口径为单个 LOD0;slide 51 的 6 毫秒是 Series S 上单个发型在被否的随机方向追踪方案下的开销,引用须同时带上「未发货」;slide 49 与 slide 51 两条都是被否方案,发货的是离线预计算;slide 34 的「非原子 load 混用原子写」按讲者原话作为规格的灰色地带保留,不写成规范做法;全篇不与任何商用方案横比