约 43 分钟技术学习游戏引擎

Games104学习笔记(31)

GAMES104 学习笔记:GPU 驱动的几何管线——Nanite(下)

课程:GAMES104《现代游戏引擎:从入门到实践》
第 22 讲(下):GPU 驱动的几何管线——Nanite(Part 2)
本地视频:22.GPU驱动的几何管线-nanite (Part 2) _ GAMES104-现代游戏引擎:从入门到实践-p01-80.mp4
视频时长:约 1 小时 37 分钟 31 秒
对应视频:B 站:GPU 驱动的几何管线——Nanite(下)

一、知识主线:Nanite 是一整套几何数据系统

上半讲建立了 GPU 驱动裁剪、间接绘制与 Visibility Buffer 的背景;下半讲开始展开 Nanite 的具体组织方式。

可以把本讲理解为以下链条:

高精度三角网格 → 构建簇与簇组 → 分组简化形成 LOD DAG → 根据屏幕误差选择表示 → BVH 加速与 GPU 调度 → 软硬件混合光栅化 → 统一可见性记录 → 材质分类与延迟着色。

这条渲染链还依赖两条资源链:

  • 几何资源链:按空间局部性分页 → 常驻粗表示 → 按需流式加载 → 紧凑编码与解码。
  • 阴影资源链:高分辨率虚拟地址 → 物理阴影页 → 按需生成 → 缓存复用与失效更新。

Nanite 不是一个孤立的“更快画三角形”的 Shader,而是让几何表示、LOD、可见性、调度和存储彼此匹配的系统。

二、为什么几何虚拟化比纹理虚拟化更难

2.1 从 Virtual Texture 获得的启发

虚拟纹理将完整纹理分为页面,通过页表映射到有限的物理纹理缓存中:

  • 虚拟地址可以描述很大的纹理;
  • 只加载当前需要的区域与 Mip;
  • 实际驻留数据不必等于全部源数据;
  • 预烘焙与缓存可以减少运行时重复工作。

几何虚拟化希望做到类似的事:允许使用高精度源资产,运行时只处理当前画面需要的细节。

但“像纹理那样虚拟化”是设计目标,不等于几何可以直接套用纹理 Mipmap 的全部规则。

2.2 几何的难点不仅是内存

增加几何细节会同时增加:

  • 数据读取;
  • 顶点变换;
  • 三角形准备;
  • 覆盖测试;
  • 深度竞争;
  • 可见性与 LOD 判断。

纹理可以进行较规则的降采样与过滤;几何简化则需要保持拓扑、轮廓、连接关系和属性连续性。

特别是不同区域采用不同精度时,还要保证接缝没有裂缝。

因此,Nanite 需要同时解决:

怎样存、怎样简化、怎样选、怎样裁剪、怎样光栅化、怎样着色。

2.3 “不再关心预算”不能按字面理解

课件先提出使用电影级源资产、不再手工逐项管理面数和 Draw Call 的愿景,再讨论现实难点。

这不代表:

  • 显存无限;
  • 带宽无限;
  • 每个三角形都无成本;
  • 任意资产布局都具有相同效率;
  • 源模型拥有数学意义上的无限精度。

更准确的理解是:系统自动管理大量细节与资源需求,减少内容制作中依靠人工维护低模与离散 LOD 的负担。

三、为什么仍然以三角网格为基础

本讲先比较了几个可能的几何表示方向。

方向 优点或适用场景 课件讨论的主要问题
体素 规则空间结构,适合某些空间查询 高精度表面存储成本、属性混合或泄漏、生产流程变化
细分曲面 适合把平滑基础曲面进一步细化 单纯细分只增加细节,不能自行降到比基础网格更简单
基于映射或位移的表示 可表现某些有机表面细节 硬表面控制、复杂连接关系与不连续表面更难处理
三角网格 已有建模、UV、材质与工具链成熟 需要为海量三角形重新设计组织与渲染方式

这里不是对体素或细分技术作普遍否定,而是在讨论:哪一种表示最适合接入既有资产生产流程,并支持大量任意形状的高精度网格?

Nanite 保留了三角形、UV 和材质等基础表达,改变的是网格内部组织与运行时处理方式。

四、Cluster:几何处理的局部单元

4.1 簇与整个对象的区别

一个对象可以有很多簇,每个簇对应一小片几何。

簇使系统能够:

  • 局部裁剪;
  • 局部选择 LOD;
  • 形成较均匀的 GPU 工作单元;
  • 独立编码和读取小块数据;
  • 在更大规模的数据中保留空间局部性。

对象仍然提供实例变换和场景生命周期;簇是内部几何单元,二者不是同一个层次。

五、为什么简单的 Cluster LOD 树还不够

5.1 最直观的方案

从精细簇开始,构建一棵树:

  • 叶子是细节较多的几何;
  • 父节点是子区域的简化表示;
  • 运行时根据视点在树上选择一个 Cut。

所谓 Cut,是选出一组节点来覆盖原始表面:

  • 每个区域选一个合适精度;
  • 不同时画某一区域的粗细两套表示;
  • 也不遗漏该区域。

这比整对象统一选择一个 LOD 更灵活。

5.2 独立简化会产生边界问题

两个相邻簇共享边界。如果分别简化,它们可能:

  • 保留不同的边界顶点;
  • 折叠不同的边;
  • 在同一位置形成不同的轮廓。

运行时一个区域用粗 LOD,另一个用细 LOD,就可能产生裂缝或不匹配。

5.3 永远锁住边界也不是好答案

为了避免裂缝,可以禁止简化簇边界。

但是随着层级变粗,原来很细的边界仍然保留,会出现:

  • 边界附近聚集大量细碎三角形;
  • 子树之间的接缝变成简化瓶颈;
  • 内部已经很粗,边界仍然过密;
  • 层级越深,累积问题越明显。

这说明只靠“独立简化 + 永久锁边”很难得到高质量、高效率的粗表示。

六、Cluster Group:把共同切换变成构建约束

课件的关键解决方式是:把相邻簇组织成一个簇组。

对某次简化操作:

  1. 将相关簇合成一个区域;
  2. 保持该区域的外边界;
  3. 允许简化原簇之间的内部共享边;
  4. 为整组建立对应的粗表示;
  5. 运行时让这次替换使用一致的 LOD 决策。

重要的不是简单地“把几个簇放在数组里”,而是:

一次粗细表示替换必须覆盖同一个区域,并保持它与组外几何相接的边界。

组内共同切换,才能安全释放原来簇间的内部边界。

这不意味着整个对象的所有簇必须始终处在同一 LOD;不同区域仍然可以根据视图需要采用不同细节。

七、离线构建:Group → Merge → Simplify → Split

7.1 一次构建操作

课件的小例子:

  • 4 个相邻簇;
  • 每个簇 4 个三角形;
  • 合计 16 个三角形;
  • 合并后简化到 8 个三角形;
  • 再拆成 2 个新簇,每簇 4 个三角形。

简化前后的区域外边界保持一致,但内部三角形和簇划分发生变化。

小图只解释机制;实际簇大小与组规模不必等于这个例子。

7.2 多层重复

整体构建过程是:

  1. 对原始三角形建立细粒度簇;
  2. 选择需要一起处理的相邻簇;
  3. 合并三角形;
  4. 简化;
  5. 重新划分成簇;
  6. 在更粗层级重复。

课件使用约减半的三角形数量说明每轮简化。真实资产受边界、形状和误差限制,不应把“每轮正好减半”当作所有输入的绝对保证。

八、交替组边界:让旧接缝有机会被简化

如果每一级都沿同样的组边界划分,旧边界仍然可能永远无法简化。

因此,课件强调在相邻层级(?)重新分组:

  • 本层是组外边界;
  • 下一层可能成为新组的内部边;
  • 本层锁住;
  • 下一层可以解锁并折叠。

这样避免了同一批接缝在所有层级上永久保留。

需要注意:这里说的是离线构建各层级表示时重新组织边界,不是运行时随便移动网格接缝。

运行时仍然消费已经构建好的兼容表示。

九、为什么是 DAG,而不是一棵普通树

DAG 是 Directed Acyclic Graph,即有向无环图。

重新分组和重新切簇后,粗簇与细簇不再总是简单的一对一父子关系:

  • 一个组中的多个细簇共同产生多个粗簇;
  • 粗簇再次组合成下一层的不同组;
  • 依赖关系会跨过原来固定子树的边界;
  • 不能把每个节点都当成仅有一个普通几何父节点。

因此,本讲用 DAG 描述这些几何表示与替换关系。

9.1 不要把 DAG 理解成任意选节点都合法

DAG 允许表达复杂关系,不意味着运行时可以独立挑出任何节点组合。

合法选择仍需要满足:

  • 覆盖完整;
  • 粗细表示不重复覆盖同一区域;
  • 组替换一致;
  • 边界兼容。

本讲后面的误差一致性与单调性,正是为了把这种复杂约束转成可并行评估的规则。

十、屏幕空间误差:LOD 应由看起来差多少决定

10.1 简化误差与观察距离

离线简化会产生几何误差。相同的世界空间误差:

  • 靠近相机时更明显;
  • 远离相机时投影更小;
  • 不同观察方向可能表现不同;
  • 屏幕分辨率与视野角也会影响像素尺度。

所以仅凭“离相机多远”不能完整描述 LOD 需要。

课件根据简化误差投影到屏幕后的影响来选择表示,并用包围球内的较坏情况修正距离和角度影响。

10.2 一个辅助理解的近似式

设:

  • $\epsilon_w$:世界空间几何误差;
  • $z$:相机空间的正向距离;
  • $H$:视口高度,单位为像素;
  • $\theta_y$:竖直视野角。

对应的像素焦距为:

$$ f_{\mathrm{px}} = \frac{H}{2\tan(\theta_y/2)} $$

对较小局部位移,在有效透视投影范围内可近似理解为:

$$ \epsilon_{\mathrm{px}} \approx \frac{f_{\mathrm{px}}\epsilon_w}{z} $$

这个式子说明“同样误差越近越大、分辨率越高越明显”。

它不是课件实际保守投影的完整实现:

  • 不能忽略包围体范围;
  • 深度方向位移和斜视会带来额外影响;
  • 近裁剪面附近不能直接套用;
  • 正交投影不遵循同样的距离缩小关系;
  • 非均匀实例缩放需要正确放大误差界。

LOD 系统需要可信的误差上界,而不是仅使用簇中心距离得出乐观估计。

十一、并行 LOD 选择:父级太粗,而自己足够精确

设:

  • $E_C$:当前簇表示的屏幕空间误差;
  • $E_P$:能够替换它的更粗表示所对应的屏幕空间误差;
  • $\tau$:可接受误差阈值。

先讨论候选细节可用、能够满足目标误差时的理想 LOD 选择,暂不考虑可见性测试与缺页兜底。课件的核心规则是:

$$ \mathrm{Render}(C) \iff (E_P>\tau)\land(E_C\leq\tau) $$

含义:

  1. 更粗表示已经不够精确;
  2. 当前表示足够精确;
  3. 当前表示恰好落在需要的精度边界上。

这里的 Render 是“按目标误差入选”的标志,不是实际绘制的全部条件。细页未驻留时,兼容粗表示仍可能需要绘制,即使它暂时不满足 $\tau$;同样,$E_C>\tau$ 只表示当前精度不足,不保证此刻已有可用的更细数据。实际提交还需要协调可见性和驻留策略,见第二十八节。

相反,LOD 排除条件为:

$$ \mathrm{LODCull}(C) \iff (E_P\leq\tau)\lor(E_C>\tau) $$

两个分支原因不同:

  • $E_P\leq\tau$:更粗表示已满足要求,当前细表示不需要;
  • $E_C>\tau$:当前仍太粗,需要更细表示。

LOD 排除不是遮挡裁剪。 前者由其他精度表示替代当前簇,后者是判断当前视图中没有贡献。

11.1 数值例子

令 $\tau=1.0$,以下数值是整理补充,不是新的课件测量:

$E_P$ $E_C$ 当前簇是否入选 原因
1.2 0.6 是 更粗不合格,当前合格
0.8 0.4 否 更粗已经够用
1.5 1.1 否 当前也不够精确,应继续细化
1.0 0.3 否 按此不等式约定,阈值相等时归更粗表示

不等式中的严格与非严格比较必须保持一致,否则阈值处可能出现重复选择或缺口。

11.2 “Parent”不是随便挑一个 DAG 入边

这里的 ParentError 是按构建时的簇组替换关系准备的共享信息,不能在多依赖 DAG 中临时任选一个粗簇作为普通树父亲。

同一次细→粗替换需要使用一致的误差与投影信息:

  • 被共同替换的细簇共享该替换所需的决策输入;
  • 对应粗表示使用匹配的当前误差;
  • 相同输入和相同规则,产生相同的替换判断。

这样避免每个线程运行时再互相通信来投票决定 LOD。

十二、误差单调性:局部判断成立的前提

课件强调:

$$ E_P \geq E_C $$

沿着从细到粗的表示关系,误差应当单调不减。

它使阈值能够定义一致的精度分界。如果某个更粗层级反而报告了更小误差,局部规则就可能选出不正确或不一致的表示。

12.1 不能只保证离线误差单调

即使离线的世界空间误差有序,运行时不同包围体、距离和角度修正也可能破坏投影误差的顺序。

因此要共同保证:

  • 离线误差的保守传播;
  • 组内决策数据一致;
  • 投影修正不破坏单调性;
  • 阈值比较一致;
  • 根与最细表示的特殊情况明确。

课件的“可并行选择”是建立在这些构建约束上的,不是说任意简化 DAG 都天然适用这两个不等式。

12.2 根与最细层

根没有真实的更粗父表示,需要单独约定。概念上可以把根的外部父误差视为无穷大,或专门处理根选择。

原始最细表示相对于自身的简化误差可以视为零,但它不意味着相对于真实物体零误差:源扫描、建模和顶点量化仍然有误差。

如果指定的精度超过源数据能力或驻留预算,系统不能凭空生成无限精度。

十三、为什么还需要 BVH

每个簇都能独立评估,不等于每帧扫描所有簇就便宜。

课件举例:数百万三角形会形成大量簇。如果逐个检查所有簇及组,候选筛选本身就可能过于昂贵。

BVH(Bounding Volume Hierarchy,包围体层次结构)用来快速排除一批不需要检查的候选。

这里要分清:

结构 主要表达什么
LOD DAG 不同几何表示之间的构建与替换关系
BVH 为候选查询与批量排除服务的加速组织
任务队列 运行时怎样把待处理节点分配给 GPU Worker

因此,不想在运行时沿复杂 DAG 逐项处理依赖,不等于完全不进行任何层次遍历。系统仍然可以遍历另外构建的 BVH。

十四、BVH4 与 ParentError 上界

课件介绍四叉的 BVH:

  • 内部节点有 4 个子节点;
  • 叶子包含某个组的簇列表;
  • 节点保存子项 ParentError 的最大值信息。

重点是“更粗表示是否已经够用”,所以不能只用当前 ClusterError 来概括整个子树的可排除性。

14.1 为什么取最大值

作为保守剪枝原理的补充,设节点 $N$ 覆盖一组候选,定义屏幕空间上界:

$$ E_N^{\mathrm{upper}} \geq \max_{i\in N} E_{P,i} $$

如果:

$$ E_N^{\mathrm{upper}} \leq \tau $$

那么所有候选的更粗表示都已满足误差阈值,这批当前细表示不需要继续检查。

反过来,上界大于阈值只表示“可能还有需要的细表示”,不能直接断言整个节点全部入选。

离线保存的误差与运行时投影必须共同维持上界保守性。不能直接把不同空间中的数值混在一起做 Max。

BVH 的包围体还可配合视锥、HZB 等空间裁剪。误差剪枝与遮挡剪枝是不同理由,应分别理解。

十五、Persistent Threads:复用 Worker,而不是不断派生线程

15.1 层次任务的负载不均衡

有些节点很快被排除,有些节点要继续展开很多子节点。

若按层级反复 Dispatch:

  • 每一层都要组织新工作;
  • 工作数量变化很大;
  • 某些批次不能充分利用 GPU;
  • 父子工作之间还需要安排依赖。

15.2 常驻线程模型

课件采用 Persistent Threads:

  1. 通过一次 Dispatch 启动一批 Worker;
  2. Worker 从队列取得节点或簇任务;
  3. 判断误差与可见性;
  4. 需要继续细查时,把后续候选加入队列;
  5. 再取得新任务;
  6. 将最终存活簇输出给渲染。

不是“Shader 创建一个新的操作系统线程”,而是在本次 GPU 调度中复用既有执行单元来消费动态任务。

“一次 Dispatch”描述该遍历阶段的组织方式,不是说整个 Nanite 帧只有一个 Dispatch。

15.3 MPMC 队列的正确性

MPMC 是多生产者、多消费者队列。

课件说明线程通过任务队列协作。作为实现边界补充,还需要处理:

  • 多线程争抢同一槽位;
  • 内容写完后再发布任务;
  • 队列容量;
  • 工作计数;
  • 线程组间可见性;
  • 终止条件。

队列短暂为空,不一定意味着所有工作完成:其他 Worker 可能仍在处理节点并准备产生新任务。

终止判断应同时确认没有已排队任务,也没有仍可能产生后续任务的在途工作。预留了队列槽位不等于任务内容已经发布,发布与消费需要相应的内存可见性保证。线程组内屏障不能代替跨组同步,也不能让已运行的线程组一直等待尚未获得调度机会的组,造成执行停滞。

常驻线程优化调度,并不免除原子操作、内存顺序和阶段同步的要求。

十六、微小三角形为什么可能不适合传统光栅化

硬件光栅化擅长批量覆盖大量像素,但三角形越来越小时,固定准备成本与执行粒度带来的浪费会变突出。

课件分别展示了:

  • 硬件的分块遍历示意;
  • 像素着色中与导数相关的 Quad 执行;
  • 三角形只贡献很少像素时的低效率。

示意中的 4×4 Tile 与 2×2 像素 Quad 是不同概念,不能混为同一个硬件粒度。

16.1 一个像素贡献可能引出额外工作

当三角形仅覆盖一个有效像素时,传统像素着色路径可能还要为导数准备相邻 Helper Lane。

Nanite 的软件路径可以把覆盖与可见性写入做得更贴近实际贡献,再在后续恢复属性和梯度。

但“面积小于一个像素”并不严格等于“只可能覆盖一个采样点”:跨像素边界的三角形仍然需要正确覆盖测试。

不能直接把所有小三角形强行缩成一个像素。

十七、软硬件混合光栅化

17.1 Software Rasterizer 仍然运行在 GPU

本讲的“软件光栅化”指在可编程 GPU Shader 中实现覆盖和深度竞争,不是把微三角形交回 CPU。

它可利用当前数据组织:

  • 从簇读取压缩几何;
  • 变换顶点;
  • 执行覆盖测试;
  • 输出统一的可见性记录。

17.2 为什么按 Cluster 选择路径

课件按簇决定采用软件还是硬件光栅化:

  • 较小的屏幕投影几何走软件路径;
  • 较大的三角形走硬件路径;
  • 两条路径写入相同的可见性表示。

课件用“簇的投影边尺度小于约 18 像素”说明软件路径选择。它是历史实现中的判据示例,不是对所有三角形面积的统一阈值,更不是当前版本永远固定的常数。

17.3 Scanline 的作用

直接遍历三角形屏幕包围矩形可能检查很多并不在三角形内的像素,细长斜三角形尤其明显。

扫描线方式按行估计覆盖区间,减少包围矩形内、三角形外的无效候选。

软件路径也有成本:

  • 顶点变换;
  • 三角形设置;
  • 覆盖测试;
  • 原子竞争。

所以并不是越“软件化”越好;混合方式让不同尺寸几何使用更合适的执行路径。

十八、64 位 Visibility Buffer:深度与身份一起竞争

18.1 本讲的格式不同于上半讲的通用例子

课件展示 Nanite 可见性记录的字段:

字段 位数 含义
Depth 32 用于可见性竞争的深度编码
Visible Cluster Index 25 当前可见簇记录的索引
Triangle Index 7 簇内三角形编号
合计 64 一个像素的可见性记录

上半讲曾介绍通用的 32 位标识示例。这里不能继续写成“Nanite 只用 32 位存储包括深度在内的全部信息”。

Visible Cluster Index 指向当前渲染所使用的记录,需要从该记录找回网格与实例信息;它不应被理解成仅仅一个会在不同实例间重复的资产内簇编号。

18.2 为什么要原子更新整个记录

很多三角形可以并行争抢同一个像素。

如果分别写深度和 ID,可能出现:

  • 深度来自三角形 A;
  • ID 来自三角形 B。

因此,把二者打包成一个可原子比较与更新的值,使获胜表面的深度和身份保持一致。

课件使用 64 位原子操作与 InterlockedMax,硬件路径也写入统一的 UAV 可见性记录。

18.3 深度排序是打包方式的前提

以下只是帮助理解字典序比较的数学表示,不是对所有版本位布局的承诺。

设 $d$ 为随表面接近观察者而增大的 32 位无符号深度编码,$c,t$ 分别是簇和三角形索引:

$$ 0\leq c<2^{25},\qquad 0\leq t<2^7 $$

可把打包值理解为:

$$ K = 2^{32}d + 2^7c + t $$

用最大值竞争时:

  • 深度位具有最高优先级;
  • 更近的有效表面胜出;
  • 相同编码深度下,低位 ID 决定一个稳定的胜者。

需要保证深度编码的整数顺序与所需的近远顺序一致。不能对任意浮点位模式直接假设“整数 Max 就是最近表面”;普通 Z、Reverse Z、异常值和清除值都需要正确处理。

未命中像素与保留标识也要单独定义。字段有多少位,不等于引擎保证能用满所有编号。

64 位原子操作的 API 与资源支持需要查询平台能力;HLSL 的相关能力可参考 Microsoft:Shader Model 6.6 Atomic Operations。这项 API 资料是补充,不代表课件各平台都采用同一后端实现。

十九、从 Visibility Buffer 恢复材质属性

课件的材质阶段需要:

  1. 读取可见性记录;
  2. 找回实例变换;
  3. 读取三角形的 3 个顶点索引;
  4. 解码顶点位置;
  5. 将顶点变换到屏幕;
  6. 求当前像素的重心坐标;
  7. 读取并插值属性;
  8. 计算材质,写入 G-Buffer。

这是用“较小的可见性记录”换取后续的几何数据访问与属性重建。

19.1 透视正确插值

设屏幕线性重心坐标为 $\lambda_i$,顶点齐次坐标分量为 $w_i$,属性为 $a_i$:

$$ a(x,y)= \frac{ \sum_{i=0}^{2}\lambda_i(x,y)a_i/w_i }{ \sum_{i=0}^{2}\lambda_i(x,y)/w_i } $$

它不是简单的屏幕线性插值。有效裁剪、非退化三角形和非零分母是公式前提。

法线还需要正确变换与归一化,UV 则需要可靠梯度供 Mipmap 和各向异性过滤使用。

19.2 梯度不能跨不相关三角形乱求

相邻屏幕像素可能属于不同表面。材质阶段若直接对恢复的 UV 做无条件相邻差分,边界处可能得到错误梯度。

需要基于有效几何关系恢复导数,或采用其他可靠的梯度策略。

显式 UV 梯度按屏幕方向配对:

$$ \frac{\partial \mathrm{UV}}{\partial x}

\left( \frac{\partial u}{\partial x}, \frac{\partial v}{\partial x} \right) $$

$$ \frac{\partial \mathrm{UV}}{\partial y}

\left( \frac{\partial u}{\partial y}, \frac{\partial v}{\partial y} \right) $$

这里 $x,y$ 是像素坐标;若在归一化坐标中求导,应换算到每像素尺度。

二十、不要把“减少无效着色”写成“没有 Overdraw”

课件一方面强调延迟材质计算减少无效像素着色,另一方面专门讨论 Rasterizer Overdraw。

二者并不矛盾。

20.1 被推迟的是昂贵材质工作

可见性确定后,材质阶段可以只围绕最终表面进行计算,而不必为之前被覆盖的每一层几何完整执行材质。

20.2 几何阶段仍然可能反复竞争

在课件展示的 UAV 原子路径中,不能直接假定拥有普通深度渲染目标的全部硬件 Hi-Z 拒绝收益。

软件 HZB 主要先裁剪簇,仍然可能留下:

  • 大簇内部不可见的三角形;
  • 相互重叠的簇;
  • 很多分离表面组成的几何;
  • 快速运动下失效的历史预测。

这些几何会继续消耗变换、覆盖测试和原子写入成本。

课件总结的瓶颈倾向是:

  • 小三角形:顶点变换与三角形设置;
  • 中等三角形:覆盖测试;
  • 大三角形:原子竞争。

这是一种历史实现中的分析框架,不是所有场景、硬件与版本上的唯一瓶颈。

20.3 一次绘制的表述范围

课件介绍把大量不透明几何集中提交,减少逐对象 Draw Call。

不能由此推导:

  • 所有材质只执行一个 Shader;
  • 阴影只有一个 Pass;
  • 软件和硬件路径不需要分别组织;
  • 整个游戏帧只有一个绘制命令。

几何提交、可见性记录、材质计算与最终光照是不同阶段。

二十一、极小实例的 Imposter

除了簇级 LOD,课件还介绍屏幕上非常小的实例可以使用预计算替身。

Imposter 保存多个观察方向下的近似投影,避免为了几乎不可分辨的整个对象再处理大量簇。

历史方案的特点包括:

  • 把多个方向放入图集;
  • 用八面体映射组织观察方向;
  • 保存深度和三角形相关信息;
  • 用少量 Ray March 调整方向变化引起的视差;
  • 从实例裁剪阶段直接处理;
  • 相关小数据常驻。

课件用了 12×12 个方向、每方向 12×12 像素说明该实现,并明确表示希望用更好的方案替代。

因此,应将它理解为历史方案针对极小实例的特定优化,而不是 Nanite 所有版本都必须拥有的固定流程。它也不等于没有任何方向量化或视差误差。

二十二、Deferred Materials:先按身份筛选,再执行材质

22.1 逐材质全屏处理的直接方案

对每个材质绘制一个全屏 Quad:

  • 检查像素是否属于本材质;
  • 不匹配就跳过;
  • 匹配则重建属性并着色。

它比较直接,但会有大量空工作。GPU 驱动后,CPU 也未必知道某个材质在本帧是否还有任何可见像素。

22.2 把 Material ID 编码成“材质深度”

课件使用硬件深度测试加速材质筛选:

  1. 将可见像素的 Material ID 编成一个数值;
  2. 写入专门用于材质判别的深度表示;
  3. 绘制某材质时,用对应编码值进行匹配测试;
  4. 不属于该材质的像素尽早被拒绝。

这利用深度硬件做分类,而不是让材质编号表示真实世界的前后距离。

必须区分:

  • 场景几何深度:决定谁挡住谁;
  • 材质深度:用作分类或匹配编码。

编码精度、材质数量、比较方式与深度写入状态都要保证匹配可靠。

二十三、Material Tile Remap Table:只处理材质出现的区域

23.1 两级筛选

全屏按材质处理仍然会访问很多完全没有该材质的区域。

课件进一步做 Tile 分类:

  • 将屏幕分成 Tile;
  • 统计每个 Tile 中出现哪些材质;
  • 对指定材质,跳过完全不包含它的 Tile;
  • 在保留 Tile 内,再用材质深度筛选像素。

Tile 判断区域是否值得处理,材质深度判断具体像素是否匹配。

一个 Tile 中可以有多个材质;标记某材质出现,不意味着 Tile 内全部像素都属于它。

23.2 课件中的位图组织

课件示例:

  • 一个 Tile 覆盖 64×64 像素;
  • 使用 16×16 线程组织该 Tile 的分类;
  • 每 32 个 Tile 组成一组;
  • 对每个材质,用一个 32 位掩码记录它在组内哪些 Tile 中出现。

线程数与像素数不同,不应理解成每个线程永远只检查一个像素。

设总 Tile 数为 $N_T$,则组数为:

$$ N_G = \left\lceil \frac{N_T}{32} \right\rceil $$

设材质槽数为 $N_M$。若采用完整的稠密位图表,其基础存储为:

$$ M_{\mathrm{table}} = 4N_MN_G $$

单位为字节;这只是位图部分,不包括索引、计数、命令或其他资源。

若屏幕宽高为 $W,H$,此示例中的 Tile 数为:

$$ N_T = \left\lceil\frac{W}{64}\right\rceil \left\lceil\frac{H}{64}\right\rceil $$

边缘不满一个 Tile 的像素范围仍需边界检查。

这些推导用于读懂课件的数据结构,不意味着所有实现都采用完整稠密表,也不意味着表规模可以忽略。

二十四、阴影为什么不直接用海量几何光追

课件讨论了当时方案中的几类压力:

  • 多光源意味着很多阴影查询;
  • Nanite 使用自定义紧凑三角形编码;
  • 硬件光追加速结构需要另一种组织;
  • 流式加载与部分更新会增加维护复杂度;
  • 额外三角形数据与 BLAS 可能扩大存储成本。

BLAS 是 Bottom-Level Acceleration Structure,即底层光追加速结构。

课件给出的容量倍数和硬件展望属于历史背景。不能将“当时不采用这条路径的理由”写成“Nanite 永远不能参与光追”或当前所有引擎版本的能力结论。

二十五、从 CSM、SDSM 到 Virtual Shadow Map

25.1 级联阴影贴图

CSM 根据相机范围划分级联,让不同距离获得不同阴影采样精度。

问题是控制粒度相对粗:

  • 每级覆盖较大区域;
  • 一部分采样可能浪费;
  • 增加细节时仍需较多内存与渲染成本。

25.2 Sample Distribution Shadow Maps

SDSM 分析屏幕可见深度分布,让阴影覆盖更贴近实际采样需求。

它改进了级联覆盖,但仍然以较粗的整体区域调节采样,不等于按需分配任意小页面。

25.3 虚拟阴影贴图的关键改变

Virtual Shadow Map 将高分辨率阴影空间拆成页面:

  • 虚拟地址可以很大;
  • 只有需要的页占用物理缓存;
  • 静态、有效的页可以跨帧复用;
  • 新需求或失效页才需要重新生成。

“虚拟”首先是地址与驻留管理,不意味着所有虚拟像素都已经分配实际深度存储。

这里的 VSM 指 Virtual Shadow Map,不是另一个常用缩写同为 VSM 的 Variance Shadow Map。

二十六、阴影页表与物理页池

课件以 16k×16k 的虚拟阴影图说明高分辨率地址空间;点光源需要考虑多个方向,示例使用 6 个面。

方向光还通过 Clipmap 组织不同空间尺度的覆盖。不能把所有光源都简单写成“只有一张相同布局的二维图”。

26.1 页表保存映射,不保存完整阴影

课件的页表项包含:

  • 物理页地址;
  • LOD 偏移;
  • 有效性信息。

查询过程可以概括为:

  1. 根据光源与接收点定位虚拟地址;
  2. 确定所需层级和虚拟页;
  3. 通过页表找物理页;
  4. 访问页内深度;
  5. 执行相应阴影比较与过滤。

未驻留或无效的映射不能直接当成可信缓存读取。

26.2 虚拟分辨率不等于物理成本

实际成本取决于:

  • 本帧请求的页面数量;
  • 常驻缓存大小;
  • 光源数量;
  • 更新量;
  • 投射物复杂度;
  • 过滤访问范围。

高分辨率虚拟地址减轻“整图全部分配”的问题,但并不自动消除很多光源和大范围动态阴影的工作。

二十七、Shadow Page Cache:什么时候能复用,什么时候会失效

课件强调很多光源相对稳定,应尽可能缓存阴影。

但“光源不动”不是唯一条件。

变化 对缓存的影响
相机平滑移动 通常逐步产生新页需求,已有有效页有机会复用
光源移动或旋转 课件方案中会使该光源相关缓存失效
投射物移动 受旧、新投影范围影响的页需要更新
投射物增加或删除 相关区域的遮挡内容改变
材质改变顶点位置 即使对象变换不变,投影几何也可能改变

页是否需要使用,与页内容是否仍然有效,是两个问题:

  • 新页需求:以前没有加载,现在需要;
  • 失效:以前的深度已经不再描述正确场景。

移动投射物时,还需清理旧位置留下的阴影,不能只更新它的新包围体范围。

作为工程条件补充,静态对象也可能因细节流入、实际采用的 LOD 表示改变而改变阴影深度。若真正用于投影的几何发生变化,需要更新受影响缓存,或明确允许的近似策略。仅页面驻留状态改变、实际投影内容未变,则不必仅因驻留变化就使缓存失效。

细长但包围体很大的对象可能使许多页失效。大量动画、变形或移动光源会降低缓存复用率。

缓存正确性必须优先于复用;历史深度不是永久真值。

二十八、几何分页与流式加载

阴影分页缓存的是光源视图深度;Nanite 几何分页管理的是网格表示数据。两者思想相似,但资源内容和失效条件不同。

28.1 Page 内容

课件列举几何页中包含:

  • 索引;
  • 顶点;
  • 包围体;
  • LOD 信息;
  • 材质表等。

组按层级与空间局部性组织,尽量让运行时需要的相关数据集中在较少页面。

28.2 根页与流式页

课件示例区分:

  • 根页:约 64 KB,包含 DAG 顶部的粗表示,常驻 GPU;
  • 流式页:约 128 KB,保存更多细节,生命周期由 CPU 侧 LRU 管理。

这些尺寸属于课程介绍的实现,不是所有版本的固定契约。

LRU 是 Least Recently Used,即优先淘汰近期较少使用的数据。但淘汰策略还需要满足页面依赖与 GPU 使用生命周期。

28.3 有粗表示可画,不等于永远满足目标误差

常驻根页保证系统在细节尚未加载时仍有可用表示。

但是:

  • 细页未驻留时,目标精度可能暂时达不到;
  • 缺页兜底允许兼容粗表示暂时超出目标误差阈值,不能直接用理想 LOD 入选公式把它排除;
  • 渲染必须选用可用且兼容的替代表示;
  • 不能把缺页当成可见几何不存在;
  • 不能随意混用不兼容的粗细区域;
  • 正在消费的页不能提前回收。

因此,LOD 决策与驻留状态必须协调。虚拟化把加载、内存和画质联系起来,而不是使它们互不相关。

二十九、内存表示:统一量化格点与簇局部编码

29.1 为什么量化还会影响接缝

若相邻簇独立使用不同坐标量化尺度,原本相同的共享顶点可能解码到不同位置。

课件强调全局量化与簇局部存储:

  • 使用一致的量化规则;
  • 簇以局部偏移或范围来减少存储;
  • 共享位置需要能一致还原。

“全局量化”不应简单理解成整个游戏世界只能使用一个超大坐标区间。关键是相关几何的量化网格与对齐约束保持兼容。

29.2 辅助理解的量化式

对单个坐标轴,设兼容量化原点为 $o$,步长为 $\Delta$:

$$ q = \operatorname{round} \left(\frac{x-o}{\Delta}\right) $$

恢复位置为:

$$ \hat{x} = o+\Delta q $$

在最近取整、未发生截断或溢出的条件下,该坐标轴误差满足:

$$ |x-\hat{x}|\leq \frac{\Delta}{2} $$

如果三个坐标轴步长不同,对应的位置误差上界为:

$$ \lVert\mathbf{x}-\hat{\mathbf{x}}\rVert \leq \frac{1}{2} \sqrt{\Delta_x^2+\Delta_y^2+\Delta_z^2} $$

这些是理解量化的通用推导,不是课件逐字给出的精度规格。

29.3 最小位数要先明确“范围”的定义

设一个簇在某坐标轴上使用的量化整数从 $q_{\min}$ 到 $q_{\max}$,两端都允许出现。可表示值数量为:

$$ N=q_{\max}-q_{\min}+1 $$

用局部偏移 $r=q-q_{\min}$ 编码时,理论所需位数为:

$$ b=\left\lceil\log_2 N\right\rceil $$

课件用 ceil(log2(range)) 表述最小位数。整理时必须区分:

  • range 指可表示值数量;
  • 还是只指最大值减最小值。

若是后者,需要补上 $+1$,否则遇到端点会少算一位。

例如,整数 0 到 7 有 8 个值,需 3 位;0 到 8 有 9 个值,需 4 位。

$N=1$ 时理论上不需要变化位,可从基准值恢复;实际格式可能仍保留元数据或固定编码开销。

29.4 为什么需要 Shader 解码

不同簇可能有不同位宽。课件把数据组织成紧凑位流,甚至不要求每个字段都字节对齐。

GPU 位流读取器按簇格式解码顶点,再供变换与着色使用。

这降低了存储和读取量,但增加解码工作,也使“直接使用统一固定顶点格式”的假设不再成立。

三十、磁盘表示与 GPU Transcode

30.1 通用压缩与几何专用变换分工

课件讨论的组合是:

  • 通用 LZ 类压缩处理重复数据;
  • 几何领域专用变换整理数据,暴露通用压缩不容易发现的冗余;
  • 加载后通过 GPU Transcode 转成适合运行时访问的表示。

磁盘格式、传输格式和 GPU 消费格式不必完全相同。

Transcode 也不等于“渲染时每读一个顶点都重新做整页解压”;页面加载转换与逐顶点紧凑格式解码是不同层次的工作。

30.2 吞吐数字的范围

课件提到 PS5 上约 50 GB/s 的 GPU 转换吞吐,以说明并行变换能力。

这个数字不是 SSD 读取速度,不是端到端流式加载速度,也不是本机性能测量。

端到端仍受以下环节约束:

  • 存储读取;
  • 压缩率;
  • 解压与转换;
  • 上传或传输;
  • 页面依赖;
  • GPU 访问与调度。

课件关于 DirectStorage 和未来直接流入 GPU 内存的表述具有历史语境,不在本笔记中据此推断当前各平台的统一支持情况。

三十一、将完整工作过程连起来

离线构建

  • 输入高精度三角网格;
  • 建立簇;
  • 分组、合并、简化与重新切簇;
  • 形成边界兼容的 LOD DAG;
  • 构造误差数据与 BVH;
  • 量化、编码并分配页面;
  • 准备常驻粗表示和可流式加载细节。

运行时几何与材质

  • 更新实例、视图与资源状态;
  • 通过 BVH 和常驻 Worker 筛选候选;
  • 根据误差、可见性和驻留状态选择簇;
  • 按簇选择软硬件光栅化;
  • 原子竞争统一的可见性记录;
  • 识别屏幕上的材质与 Tile;
  • 重建属性和梯度;
  • 材质着色写 G-Buffer;
  • 接入后续延迟光照。

阴影与资源维护

  • 根据接收表面需求定位虚拟阴影页;
  • 复用仍有效的缓存;
  • 更新新需求与失效页;
  • 管理几何页的请求、加载、转换和回收;
  • 保证页面使用与 ID 数据的生命周期一致。

这不是代码级执行顺序承诺,而是本讲各个机制之间的依赖关系。

三十二、容易混淆的概念

容易混淆的说法 正确理解
虚拟化 = 无限内存 虚拟地址与有限物理驻留分离
Nanite = 一个新光栅化 Shader 还包括几何构建、LOD、调度、分页和编码
每个簇可以任意独立切 LOD 必须遵守簇组替换与边界兼容
锁边就彻底解决简化 永久锁边会积累过密接缝
DAG 与 BVH 是同一棵树 DAG 表达几何替换,BVH 加速候选查询
ParentError 是随便一个父节点的误差 它来自构建好的组替换关系
LODCull = 被挡住 LOD 排除由其他精度表示替代,不是遮挡结论
世界误差有序就足够 运行时投影修正也要维持单调与保守性
BVH4 = 每簇 4 个三角形 它指内部节点最多组织 4 个子项
Persistent Threads = 不需要同步 队列发布、消费与终止仍需正确同步
软件光栅化 = CPU 光栅化 本讲软件路径仍然在 GPU Shader 上执行
微三角形永远只覆盖一个像素 边界和采样布局仍需精确覆盖判断
可见性缓冲没有 Overdraw 几何仍可能重复测试和原子竞争
32 位通用标识 = Nanite 的 64 位记录 两个课件例子的字段与成本不同
InterlockedMax 总是得到最近深度 取决于编码的排序方向与无效值定义
Material Depth = 场景深度 前者用于材质分类,后者用于几何遮挡
Tile 有材质 = 所有像素都是该材质 Tile 只是粗筛,像素仍需匹配
光源不动 = 阴影永不失效 投射物或顶点位置变化也会使页失效
根页常驻 = 永远达到目标精度 它保证可用粗表示,不保证细节已加载
GPU 转换吞吐 = 磁盘速度 属于不同环节,端到端还受其他约束
量化位数 = log2(max-min) 包含两端时,值数量应为 max-min+1
本讲的 VSM = Variance Shadow Map 此处是 Virtual Shadow Map

三十三、复习要点

  1. 虚拟几何需要同时控制计算与存储。 仅做分页不能解决大量微三角形的渲染成本。
  2. 边界兼容必须在构建时设计。 簇组共同替换、交替边界与重新切簇是核心。
  3. 并行 LOD 依赖一致、单调的误差数据。 更粗不够精确、当前足够精确,才选择当前表示。
  4. BVH 减少候选数,常驻线程分配不均匀工作。 二者不是几何 DAG 的替代定义。
  5. 混合光栅化按几何屏幕尺度选择执行方式。 软件路径在 GPU 上,大几何仍利用硬件。
  6. 深度与身份应一起竞争。 64 位记录让最终可见表面的深度和 ID 保持一致。
  7. 延迟材质与几何 Overdraw 是两回事。 几何阶段仍有覆盖与原子成本。
  8. 材质深度加 Tile 分类形成两级筛选。 尽早跳过无关区域与像素。
  9. 虚拟阴影是按需分页且可复用的深度缓存。 必须显式处理失效。
  10. 压缩不免费,细页也不必然驻留。 画质、内存、加载与解码需要一起管理。

用一句话总结:

Nanite 通过兼容的分层几何表示选出当前真正需要的细节,用 GPU 高效确定可见表面,再把材质、阴影与资源驻留组织成相互配合的按需系统。

参考资料

本讲来源

原始技术分享与补充核对