虚拟化几何工程 2026:Nanite 范式的统一装配
把百万三角形场景切成 cluster meshlet 后用 GPU hierarchical software culling 反向驱动渲染,配合 visibility buffer 解耦几何与材质,把传统 CPU draw call 范式升级为 GPU 驱动、像素驱动的虚拟化几何流水线。
约 35 分钟阅读10,437 字5 次阅读博主

把百万三角形场景切成 cluster meshlet 后用 GPU hierarchical software culling 反向驱动渲染,配合 visibility buffer 解耦几何与材质,把传统 CPU draw call 范式升级为 GPU 驱动、像素驱动的虚拟化几何流水线。

过去十五年里,实时渲染管线的工程现实一直被一条朴素的经验法则束缚——「三角形预算 × 屏幕像素覆盖率 = 性能上限」。一款 3A 游戏的角色网格大约 8 万三角形,单个场景 50–200 个网格就已经触及 16ms 的 CPU draw call 提交瓶颈。围绕这条法则,引擎工程师建立了整套工具链:LOD(多细节层级)跳变、遮挡剔除(CPU 端 frustum + HiZ occlusion)、静态合批、动态合批、GPU instancing、SRV batching。它们在不同年代各自解决了某个侧面的问题,但从未正面回答一件事:当美术希望场景中每一根树枝、每一片叶子、每一颗螺丝都真实可点选且不闪烁时,传统范式会发生什么?
UE5 Nanite 给出的答案是:抛弃「按几何对象提交」的范式,转向「按屏幕像素需求提交」。把整个静态场景在离线阶段切分为几百上千万个 cluster(meshlet),运行时由 GPU 端 hierarchical software culling 决定每个 cluster 投影到屏幕后实际贡献多少像素,按贡献度反向驱动渲染——贡献不足的 cluster 在 vertex stage 之前就被整体剪掉。这一思路并不依赖任何硬件专属特性(mesh shader 在 2026 年仍只在部分桌面端硬件普及),因此它构成了一个可移植的虚拟化几何范式。
本文要解决的问题是:在 2026 年的工程环境下,如何把 Nanite 的设计思想系统性地移植到自研引擎或现有商业引擎(Unity SRP、Godot 4、自研 Vulkan/Metal 渲染器)的私有渲染管线里,使其不再是一个「只有 UE5 才有的黑魔法」。我们把它拆成四件事——clustered meshlet 切分、visibility buffer 几何表达、hierarchical software culling、GPU 驱动渲染闭环——并给出每个环节的可执行工程取舍、模块化的依赖关系、坑位与 12 个月的落地时间表。
任何严肃的虚拟化几何实现都必须满足以下四条不变式。我们把这四条作为后续章节工程决策的硬性约束:
这四条不变式并非孤立存在——它们共同构成一个稳定的设计空间。违背其中任意一条,要么性能崩(pixel-driven 不成立 → 三角形白白浪费),要么工程复杂度爆炸(CPU 重新成为瓶颈),要么管线失去架构清晰度(GDI 与 shading 耦合 → 后处理与材质调试互相阻塞)。下面三节分别展开三条不变式的工程实现路径。
Clustered meshlet 切分的目标是把一个任意拓扑的三角网格切成 64–256 个三角形一组的小 cluster,每组满足三个约束:① cluster 内部三角形法向连续(避免跨越尖锐边切分);② cluster 包围盒体积与表面积比值在合理区间;③ 任意 cluster 内边长差异不超过 8 倍。满足这三个约束,运行时 hierarchical culling 才能对 cluster 做粗粒度 LOD 判断并避免过度绘制(overdraw)。
切分算法的工程现实里,最稳定的不是聚类,而是"求凸性优先 + 边长惩罚 + 区域均衡"三目标优化。Nanite 公开的 Karis(2014)算法把问题分解为四个独立的目标函数:覆盖度(cluster meshlet coverage = cluster 包围盒在屏幕投影中能覆盖的最大像素数,决定单 cluster 的最坏情况)、边长方差(cluster 内最长边 / 最短边的比值,决定 silhouette 的稳定性)、法向一致性(cluster 内相邻三角形法向的点积均值,决定 silhouette 的几何稳定性)、面积均衡(每个 cluster 的三角形数方差,决定 warp 内分支发散程度)。具体的算法实现里,没有任何一个单一目标是绝对主导——例如头发和树叶场景里 coverage 是第一目标(必须细密切分才能避免 silhouette 跳变),但室内硬表面场景里 coverage 反而是次要目标(边长一致性更重要,否则门框会跳变)。
工程上的取舍集中在"切分粒度"这个旋钮。粒度太粗(cluster 含 1000+ 三角形)→ hierarchical culling 的剪枝能力弱,过度绘制增加 30–50%;粒度太细(cluster 含 8–16 三角形)→ cluster 数量爆炸(10 亿三角形切分到 5 亿 cluster),indirect draw command buffer 体积爆炸,CPU→GPU 数据传输反而成为新瓶颈。Nanite 公开的经验区间是 64–128 个三角形/cluster——这个区间下 1 亿三角形的场景产生约 8000 万 cluster,对应 1.5 GB 的 cluster metadata buffer,恰好落在 2026 年主流 GPU(24 GB 显存)的内存预算内。
切分算法的实现路径有三种:① meshoptimizer(开源)库提供 cluster meshlet 切分,覆盖三个基本约束但未做 silhouette 优化;② UE5 自研切分器闭源但通过 Nanite Streaming 工具链暴露参数;③ 自研切分器(如 Frostbite、Half-Life: Alyx 的自研实现)通常以 meshoptimizer 为基线加 silhouette 优化层。2026 年的工程现实里,对中型团队而言 meshoptimizer + silhouette post-process 是 ROI 最高的路径——前者覆盖 80% 切分质量,后者补足 15%(剩下 5% 的极差情况无法用离线算法修,需要美术手动修高模)。
Visibility buffer 是 Nanite 风格虚拟化几何的第二条不变式的具体实现——它把"哪些像素被哪些三角形覆盖"这件事从材质 shading 里彻底剥离出来,单独做成一个 pass。传统的 deferred shading 把 G-buffer(position + normal + albedo + roughness + metallic + ao 等)写到一个大纹理里,再用一个全屏 pass 解算光照。Nanite 风格的 visibility buffer 则把 G-buffer 缩减到最小:每个像素只记录 (primitive_id, triangle_id, barycentric_coord) 三元组。primitive_id 指向 cluster,triangle_id 指向 cluster 内的三角形,barycentric_coord 用于在 deferred material pass 里插值顶点属性。
这条设计带来三个工程优势:① G-buffer 体积从 MB 级降到 KB 级——传统 G-buffer 8 个 RGBA8 + 2 个 RGBA16F 大约 80 bytes/pixel,4K 分辨率下 26 MB;visibility buffer 仅 8 bytes/primitive(cluster 24bit + triangle 8bit)+ 4 bytes/barycentric,4K 分辨率下约 12 MB(前提是几何覆盖不密集)。② 材质解耦让 shading pass 可以独立 scale——visibility buffer 生成后,材质 pass 既可以是 fullscreen 也可以是 compute shader,既可以全场景也可以按 cluster 切片(visibility buffer 的稀疏版本只更新被新可见的像素)。③ 抗锯齿和材质异常处理变得局部化——silhouette 上的像素跳变只发生在 cluster 边界,可以通过 cluster-aware 的 dither pattern 抑制。
但 visibility buffer 也有三个工程坑:① MSAA 不再适用——barycentric coord 在多采样点上不同,MSAA 必须在 visibility generation 阶段就做掉或完全放弃;现代 TSR/DLSS 的存在让 MSAA 在 4K 下不再是必需品;② 材质次表面散射(SSS)、透明、头发这种"跨越多个三角形"的材质无法用单一 triangle_id 表达——必须通过 cluster group ID(把多个相邻 cluster 标记为一个 material unit)或独立的 material buffer(visibility buffer 仅存几何,材质 buffer 单独管理)解决;③ 深度缓冲必须独立维护——visibility buffer 不直接写 depth,必须从 triangle 列表重新构造 depth,这是 visibility buffer 范式最隐蔽的性能瓶颈。
工程落地路径上,2026 年的引擎主要分两类:① 完整 visibility buffer(Nanite 模式):全场景走 visibility buffer + deferred material,UE5、Lumen、Strata 都属于此类;② 混合 visibility buffer(小颗粒度场景):可见性敏感的场景(远景、植被、密集道具)走 visibility buffer,主场景(玩家视线内的硬表面)仍走传统 forward+deferred,Godot 4、自研引擎大多走此路径——成本更低、对硬件要求更宽松、迁移路径更平滑。
Hierarchical software culling 是 Nanite 的"软件"二字的具体体现——它把传统由 CPU 做的 frustum + occlusion culling 完全搬到 GPU 上执行,由 GPU 在 vertex stage 之前根据 cluster 的层级包围盒(Hierarchical BV)做剪枝。核心数据结构是两个深度缓冲:传统的 GPU depth buffer(用于 early Z 和最终 depth test)和HiZ buffer(Hierarchical Z,由 GPU 在 compute pass 里从 depth buffer downsample 而来,mip 层级记录对应区域的最远深度);任何材质 instance 的 cluster 级包围盒在屏幕空间投影后,对比 HiZ buffer 即可得到「我的几何是否完全被已有深度遮挡」,完全遮挡的 cluster 直接剪掉。
HiZ culling 的关键工程细节是两个独立 pass 的同步问题。场景渲染通常分主场景(主场景剔除结果,先 scene 被填充到 depth buffer)和前景场景(角色、特效、UI,通常最后渲染),两者之间需要一个 synchronization point 确保前景可见性不会被主场景错误遮挡。在 2026 年的工程实践里,这个同步通常通过两个 visibility pass实现:第一遍主场景渲染只输出 visibility buffer 和 depth buffer,不写最终颜色;第二遍前景 + UI 直接读 depth buffer 决定早 Z 顺序——这意味着 CPU 端多提交一遍 scene graph,但 GPU 端的剪枝能力提升 30–60%,整体帧时间下降 10–25%。
Meshlet DAG(Directed Acyclic Graph)是 hierarchical software culling 的第二个隐藏支柱。每个 cluster 在切分时不仅记录自己的 BV,还记录与哪些 cluster 在空间上相邻(parent cluster 在低洼、child cluster 在高洼)。GPU 端 culling pass 按 parent-first 顺序遍历 DAG:parent cluster 被 culling 剪掉则所有 child cluster 都不需要遍历——这个 DAG 跳过机制在 1 亿 triangle 场景下能把实际遍历的 cluster 数从 5000 万降到 800 万(约 6× 剪枝率),间接绘制次数从 5000 万降到 800 万,CPU→GPU 的 command buffer 体积同步下降,渲染管线整体效率提升 20–40%。
工程现实里,hierarchical software culling 最容易踩的坑是HiZ buffer 的不一致性。当一个物体刚被移动(角色走动、相机转动),它的 depth 还没写进 HiZ buffer mip 层级,culling 把它错误地剪掉导致它"闪烁"。解决方案是两帧延迟的 mip generation——第 N 帧用第 N-2 帧的 HiZ buffer 做 culling,第 N 帧渲染后第 N 帧自身用于生成下一帧的 HiZ buffer。这种延迟换来的稳定性是大多数引擎的默认选择,但代价是角色刚进入场景的前两帧可能"晚出现"一帧,需要 fade-in 动画补足视觉。
把 nanite 范式推到工业级的桥梁是 GPU 驱动渲染闭环——CPU 端不再直接发起 draw call,只提交"渲染意图"(相机、剔除预算、时间戳),所有具体 draw call 由 GPU 端 compute shader 生成。这个范式依赖三个核心机制:Indirect Draw(GPU 通过 read-only buffer 决定 draw call 参数,避免 CPU 端 GPU 端来回切换)、Persistent Threads(GPU 端 worker 池常驻,避免每次绘制重新创建 compute kernel)、Resource Residency Manager(资源生命周期由 GPU 跟踪,CPU 端只声明"哪些资源在某帧可能用到",具体使用由 GPU 决定)。
Indirect Draw 的工程核心是 Multi-Draw Indirect——一次 API 调用提交 N 个 draw call,每个 draw call 的 vertex count、instance count、start vertex location 都从 GPU buffer 里读。Vulkan 的 vkCmdDrawIndexedIndirect、Metal 的 drawIndexedPrimitivesIndirect、DX12 的 ExecuteIndirect 都支持多 draw indirect。Nanite 在一帧内通常发起 2000–5000 个 indirect draw,每个对应一个 cluster group(多个相邻 cluster 合并绘制),单帧 command buffer 体积控制在 64–256 KB,PCIe 上传带宽占用 < 1%,这才是 GPU 驱动渲染真正能 scale 的关键。
Persistent Threads 是 compute shader 范式的高级应用:把 GPU 的 compute unit 视为常驻 worker 池,每个 worker 监听一个 atomic counter 决定自己要处理的 cluster,atomic counter 由 CPU 在帧开始时初始化。这种范式相比传统的"每帧 dispatch 一次 compute"能省去 kernel launch overhead——一次 launch overhead 大约 30–50 μs,60fps 下累计 1.8–3 ms,相当于总帧时间的 10–18%。Persistent Threads 通过一次 kernel launch 启动后让 worker 监听新 counter,避免重复 launch overhead。但 Persistent Threads 在 DX12 上有兼容性限制(需要 SM 6.6+ 的 cooperative matrix),在移动端(特别是 Adreno 6xx 之前的设备)不支持——这是 2026 年大多数移动端引擎仍走"每帧 dispatch"的主要原因。
Resource Residency Manager 是 GPU 驱动渲染的最后一公里。CPU 端传统渲染要求"渲染某个 mesh 时 vertex buffer 必须已 mapped 进 GPU 显存";GPU 驱动渲染要求"GPU 端 worker 处理某个 cluster 时它的 vertex buffer 必须已在 GPU 显存"——这两个要求看起来一样,实际区别在于 GPU 端 residency 由谁决定。传统范式里 CPU 决定(每帧开始时把要用的 mesh 物化进显存),GPU 驱动范式里 GPU 决定(基于 LRU 和 frame budget 自动 swap in/out)。后者需要在 GPU 端建一个 residency queue,由 worker 在处理 cluster 前 atomic-check 该 cluster 的资源是否在 GPU 显存里,不在则发起 residency request,CPU 端在帧间缝里处理 residency request 并把数据 DMA 进显存。这种 residency 模型让 mesh streaming 不再依赖 CPU 端的「猜哪些 mesh 这帧会被用到」调度,转而依赖 GPU 端的实际访问 pattern——典型场景下显存占用下降 20–40%。
把虚拟化几何从一个 prototype 推到 production-grade 是一个 12 个月起步的工程周期。下面是基于 2024–2026 年多个公开案例(Frostbite、Half-Life: Alyx、UE5 Nanite、Unity SRP 自研)总结的可执行时间表。
第 1–3 月:Meshlet 切分器与离线工具链。搭建 meshoptimizer + silhouette post-process 切分管线,建立 cluster metadata(BV、parent DAG、triangle 列表)的序列化格式,导入 50 个有代表性的测试 mesh(室内 / 室外 / 角色 / 植被 / 硬表面)。里程碑:能在离线工具里可视化每个 cluster 的质量 + silhouette。常见坑:切分器在硬表面场景质量很高但在头发 / 树叶场景失败,需要为后者专门写 silhouette-aware 切分器。第二个常见坑:cluster metadata 序列化格式选错——JSON 体积过大(一个 1 亿三角形的场景产生 80 GB JSON)、二进制自定义格式维护成本高。最稳定的格式是 meshoptimizer 自带的 glTF meshopt extension,体积适中、工具链齐全。
第 4–6 月:Visibility buffer 渲染管线原型。基于现有 forward+deferred 渲染器加 visibility generation pass(vertex shader 输出 cluster_id + triangle_id,pixel shader 输出 barycentric coord)和 deferred material pass(fullscreen compute shader,从 visibility buffer 读取 primitive_id 后查 vertex attribute texture,做材质 shading)。里程碑:能在简单测试场景(一个静态 cube + 10 个静态 sphere)下正确输出 visibility buffer 并解算材质。常见坑:vertex attribute texture 的格式选错——顶点属性通常 32–64 bytes/vertex,1 亿 vertex 占 3–6 GB,需要稀疏纹理 + streamed block layout。第二个常见坑:visibility generation pass 的 vertex shader 输出(primitive_id + triangle_id + barycentric)写入 RT 时未做 atomic 操作导致多线程 race condition。
第 7–9 月:Hierarchical software culling 与 indirect draw。实现 HiZ buffer generation(compute shader 从 main depth buffer downsample 到 mip chain),实现 meshlet DAG culling(parent-first traversal + atomic counter 剪枝),实现 multi-draw indirect 把批次打包。常见坑:HiZ mip 0 和 main depth buffer 的同步窗口选错,导致角色刚进入场景时 culling 把它剪掉——两帧延迟方案在前 9 个月里要花 4–6 周调试。第二个常见坑:indirect draw command buffer 体积过大(单帧 1–4 MB),CPU→GPU 上传带宽成为瓶颈——必须做 command aggregation,把相邻 cluster 合并为一个 indirect draw。
第 10–12 月:GPU 驱动渲染闭环 + resource residency。引入 persistent threads compute kernel、resource residency queue、LRU-based mesh streaming。里程碑:在 production-grade 场景(一个典型 3A 关卡,1–3 亿三角形)下稳定 60fps、显存占用 < 8 GB、CPU 占用 < 30%。常见坑:persistent threads 在 DX12 上要求 SM 6.6,需要降级到 fallback 路径——这条 fallback 必须从第 1 个月就预留,否则到第 12 月才发现兼容性问题的代价太高。第二个常见坑:resource residency queue 的 LRU 实现选错——按 access frequency 而非按时间 LRU,对 streaming 场景更稳定。
跨年度对比看,UE5 Nanite 从 2018 年 prototype 到 2022 年 production-grade 用了 4 年;Frostbite 的 meshlet pipeline 从 2021 年到 2025 年用了 4 年;自研引擎(无引擎基线)通常需要 18–24 个月——直接基于 UE5 源码移植 Nanite 子系统的方案不可取(UE5 闭源,且依赖 Unreal-specific 的 PSO mesh shard graph),可行路径是 fork UE5 + 抽出 Nanite subsystem 到独立 module,这个路径大约 12 个月可行。
虚拟化几何不是万能解药,三类场景它处理得不好或根本不适合。第一类是次表面散射(SSS)、头发、毛发这类需要跨多个三角形累积效果的材质。visibility buffer 只记录单一 triangle_id + barycentric,无法表达一个像素由多个三角形叠加贡献的情况。Nanite 的解决方案是把头发这类材质从 virtualized geometry 中分离出来走传统 forward 管线,但 hair count 仍然受三角形预算限制——UE5 在 LOTR: Gollum 里限制了头发三角形数到 5 万根,Lumen 渲染头发仍会出现弱 X 光现象。工程取舍上,长头发 / 长胡子场景虚拟化几何需要 hybrid 方案:头发 / 胡子用传统 ray-marching + strand primitive,眉毛 / 睫毛用 visibility graph + screen-space strand。
第二类是透明物体。Visibility buffer 不输出 alpha 通道——alpha 必须独立记录在 material buffer 里。但 alpha blending 必须在 visibility generation 之后才能做,导致透明物体必须从主场景 visibility pass 里分离出来,单独走一个 forward blending pass。这意味着透明物体无法受益于 hierarchical software culling——它们的 culling 仍由 CPU 端提交。工程取舍:场景中透明物体占比 < 5% 时可以接受,> 15% 时虚拟化几何的 overdraw 节省会被 transparency 拖累,整体性能反而不如传统 deferred。
第三类是比像素还小的细节。一个 1 像素的三角形无法贡献有效 shading 信息,但 visibility buffer 仍会把它写进 primitive_id。这部分"亚像素贡献"在 hierarchical culling 里通常被早 Z cene 掉(贡献小于 1 pixel 的 cluster 在 software culling 第二层被剪掉),但 1 pixel 边缘的细节(贡献高微小但可见的细节,如屏幕反走样的硬表面边线)容易被错误剪掉。Nanite 公开承认在亚像素场景下 silhouette 质量下降,UE5 在 1K 分辨率下不推荐开启 Nanite——必须 4K 或者 TSR/DLSS 上采到 4K 后才稳定。
第四类是动态几何。Hierarchy DAG 假设几何不可变(不变式 1),动态几何(破坏型变更、刚体模拟、变形 mesh)必须走传统 dynamic mesh pipeline。UE5 的解决方案是把静态 + 动态分开:静态用 Nanite,动态用传统的 skeletal mesh / mesh component。Frostbite 的解决方案是 hybrid dynamic mesh:每帧把动态 mesh 的 LOD 重新计算 cluster,但这会破坏 invariant 导致 hierarchical culling 失效——只对 1000 三角形以下的小动态 mesh 可行。
第五类边界条件是几何密度变化剧烈的场景。一座城市里既有 100 万三角形的高楼,又有 1000 三角形的路灯,hierarchical culling 的 DAG 跳过率对前者有效(剪枝掉 80×)但对后者失效(剪枝 1×)。这种情况下虚拟化几何的 overdraw 节省主要集中在前者,路灯这类小物体反而被 visibility buffer 的 metadata overhead 拖累——总帧时间不一定比传统 forward+deferred 快。工程取舍:场景几何密度 CV(coefficient of variation)> 10 时考虑 hybrid pipeline(小物体走传统 forward,大物体走 visibility buffer)。
下面是从今天起可以执行的 12 步,每步对应一个具体的工具调用或工程产出。
每步完成后跑一次 RenderDoc capture + 量化(三角形数 / draw call 数 / overdraw / frame time),确认确实提升后再进入下一步。任何一步不带来可量化的提升都该停——虚拟化几何不是目的,性能提升才是。
一句话摘要:Nanite 风格的虚拟化几何通过把静态几何切分为百万级 cluster、由 GPU 端 hierarchical software culling 按像素需求反向驱动渲染,并配合 visibility buffer 解耦几何与材质,可在 2026 年的主流 GPU 上稳定支撑 1–3 亿三角形场景的 60fps 实时渲染,但需要 12 个月以上的工程投入且仅对静态 / 硬表面 / 高密度几何场景收益最大。
Conversation
0 条