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:把共同切换变成构建约束
课件的关键解决方式是:把相邻簇组织成一个簇组。
对某次简化操作:
- 将相关簇合成一个区域;
- 保持该区域的外边界;
- 允许简化原簇之间的内部共享边;
- 为整组建立对应的粗表示;
- 运行时让这次替换使用一致的 LOD 决策。
重要的不是简单地“把几个簇放在数组里”,而是:
一次粗细表示替换必须覆盖同一个区域,并保持它与组外几何相接的边界。
组内共同切换,才能安全释放原来簇间的内部边界。
这不意味着整个对象的所有簇必须始终处在同一 LOD;不同区域仍然可以根据视图需要采用不同细节。
七、离线构建:Group → Merge → Simplify → Split
7.1 一次构建操作
课件的小例子:
- 4 个相邻簇;
- 每个簇 4 个三角形;
- 合计 16 个三角形;
- 合并后简化到 8 个三角形;
- 再拆成 2 个新簇,每簇 4 个三角形。
简化前后的区域外边界保持一致,但内部三角形和簇划分发生变化。
小图只解释机制;实际簇大小与组规模不必等于这个例子。
7.2 多层重复
整体构建过程是:
- 对原始三角形建立细粒度簇;
- 选择需要一起处理的相邻簇;
- 合并三角形;
- 简化;
- 重新划分成簇;
- 在更粗层级重复。
课件使用约减半的三角形数量说明每轮简化。真实资产受边界、形状和误差限制,不应把“每轮正好减半”当作所有输入的绝对保证。
八、交替组边界:让旧接缝有机会被简化
如果每一级都沿同样的组边界划分,旧边界仍然可能永远无法简化。
因此,课件强调在相邻层级(?)重新分组:
- 本层是组外边界;
- 下一层可能成为新组的内部边;
- 本层锁住;
- 下一层可以解锁并折叠。
这样避免了同一批接缝在所有层级上永久保留。
需要注意:这里说的是离线构建各层级表示时重新组织边界,不是运行时随便移动网格接缝。
运行时仍然消费已经构建好的兼容表示。
九、为什么是 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) $$
含义:
- 更粗表示已经不够精确;
- 当前表示足够精确;
- 当前表示恰好落在需要的精度边界上。
这里的 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:
- 通过一次 Dispatch 启动一批 Worker;
- Worker 从队列取得节点或簇任务;
- 判断误差与可见性;
- 需要继续细查时,把后续候选加入队列;
- 再取得新任务;
- 将最终存活簇输出给渲染。
不是“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 恢复材质属性
课件的材质阶段需要:
- 读取可见性记录;
- 找回实例变换;
- 读取三角形的 3 个顶点索引;
- 解码顶点位置;
- 将顶点变换到屏幕;
- 求当前像素的重心坐标;
- 读取并插值属性;
- 计算材质,写入 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 编码成“材质深度”
课件使用硬件深度测试加速材质筛选:
- 将可见像素的 Material ID 编成一个数值;
- 写入专门用于材质判别的深度表示;
- 绘制某材质时,用对应编码值进行匹配测试;
- 不属于该材质的像素尽早被拒绝。
这利用深度硬件做分类,而不是让材质编号表示真实世界的前后距离。
必须区分:
- 场景几何深度:决定谁挡住谁;
- 材质深度:用作分类或匹配编码。
编码精度、材质数量、比较方式与深度写入状态都要保证匹配可靠。
二十三、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 偏移;
- 有效性信息。
查询过程可以概括为:
- 根据光源与接收点定位虚拟地址;
- 确定所需层级和虚拟页;
- 通过页表找物理页;
- 访问页内深度;
- 执行相应阴影比较与过滤。
未驻留或无效的映射不能直接当成可信缓存读取。
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 |
三十三、复习要点
- 虚拟几何需要同时控制计算与存储。 仅做分页不能解决大量微三角形的渲染成本。
- 边界兼容必须在构建时设计。 簇组共同替换、交替边界与重新切簇是核心。
- 并行 LOD 依赖一致、单调的误差数据。 更粗不够精确、当前足够精确,才选择当前表示。
- BVH 减少候选数,常驻线程分配不均匀工作。 二者不是几何 DAG 的替代定义。
- 混合光栅化按几何屏幕尺度选择执行方式。 软件路径在 GPU 上,大几何仍利用硬件。
- 深度与身份应一起竞争。 64 位记录让最终可见表面的深度和 ID 保持一致。
- 延迟材质与几何 Overdraw 是两回事。 几何阶段仍有覆盖与原子成本。
- 材质深度加 Tile 分类形成两级筛选。 尽早跳过无关区域与像素。
- 虚拟阴影是按需分页且可复用的深度缓存。 必须显式处理失效。
- 压缩不免费,细页也不必然驻留。 画质、内存、加载与解码需要一起管理。
用一句话总结:
Nanite 通过兼容的分层几何表示选出当前真正需要的细节,用 GPU 高效确定可见表面,再把材质、阴影与资源驻留组织成相互配合的按需系统。
参考资料
本讲来源
- B 站:GAMES104 GPU 驱动的几何管线——Nanite(下):与本地 Part 2 对应。
- GAMES104 官方课程列表:核对上下半段视频与标题。
原始技术分享与补充核对
- SIGGRAPH 2021:A Deep Dive into Nanite Virtualized Geometry:Brian Karis、Rune Stubbe、Graham Wihlidal 的原始技术分享入口;本次未下载其 PDF。
- Graham Wihlidal:Nanite Deep Dive 项目页:原始分享的作者入口。
- Microsoft:HLSL Shader Model 6.6 Atomic Operations:64 位原子操作及资源支持的 API 补充。
- Epic Games:Virtual Shadow Maps:当前文档入口,查阅时注意版本差异;本笔记的历史参数以本地课件为准。