UE5.8 Nanite 虚拟几何体:把手工 LOD 扔进历史的垃圾桶

📑 目录

UE5.8 Nanite 虚拟几何体:把手工 LOD 扔进历史的垃圾桶

引子:老周的岩石资产

美术组长老周记得很清楚,2023 年那个夏天,他花了整整三天给一块英雄级岩石雕 LOD。LOD0 八十三万面,LOD1 二十一万面,LOD2 五万面,LOD3 八千面。每一级都要手动减面、修 UV、烘法线,最后还要在引擎里一个个设置切换距离。

「这块石头很重要,玩家会贴着它走。」策划说。

三个月后,关卡改了,相机距离变了,LOD 切换点全得重调。老周盯着屏幕,第一次认真思考:「为什么我要当一台人肉减面机?」

UE5 给出的答案叫 Nanite。到 5.8 版本,这套系统已经成熟到足以支撑绝大多数静态几何体管线。本文不聊营销话术,只聊它到底怎么工作、什么时候该开、什么时候得关。


一、手工 LOD 的账,算起来很疼

在理解 Nanite 之前,先看看它要取代的东西。

传统 LOD 管线的核心假设是:一个模型在远处不需要那么多面。所以美术要准备多个版本,从精细到粗糙,引擎根据距离切换。这个思路简单直接,但成本悄悄摊在很多地方:

  • 美术时间:每个重要资产要做 3-5 级 LOD, photogrammetry 扫描件还要 retopo;
  • 内存占用:LOD0 到 LOD3 全要常驻显存,远处物体也不能完全卸载;
  • Draw Call:每个 LOD 层级通常对应一个 draw call 或 instance;
  • 切换裂缝:LOD 切换点没调好,玩家会眼睁睁看着石头「跳」一下;
  • 反复调整成本:相机距离一变,切换距离就得重调。

更麻烦的是,LOD 的粒度是「整个模型」。一块石头上离相机很近的凸起和远处的平面,必须共享同一个 LOD 级别。这意味着要么局部过糊,要么整体浪费。

Nanite 把粒度从「整个模型」降到了「三角形集群」。


二、Nanite 的核心思路:只渲染看得见的三角形

Nanite 不准备离散 LOD。它把原始网格预处理成一组层级化的三角形集群(cluster),每个 cluster 最多 128 个三角形。这些 cluster 组成一棵有向无环图(DAG),父节点是低细节版本,子节点是高细节版本。

flowchart TD
    A[原始网格 500 万三角形] --> B[切分为 Cluster]
    B --> C[构建层级 DAG]
    C --> D[父 Cluster 简化版]
    C --> E[子 Cluster 细节版]
    D --> F[运行时按屏幕大小选择]
    E --> F
    F --> G[只渲染可见 Cluster]

运行时,GPU 做三件事:

  1. 可见性判断:用 HZB(Hierarchical Z-Buffer)剔除被遮挡的 cluster;
  2. LOD 选择:根据每个 cluster 在屏幕上的投影大小,决定用父节点还是子节点;
  3. 光栅化与着色:把选中的 cluster 送进 VisBuffer + BasePass。

关键在这里:不同 cluster 可以处于不同细节级别。玩家贴着的凸起用高密度 cluster,远处的平面用低密度 cluster,不需要统一切换。

architecture-beta
    group gpu[GPU]
    group cpu[CPU]
    
    service importer as 导入时构建 Cluster DAG in cpu
    service hzb as HZB 遮挡剔除 in gpu
    service lod as Cluster LOD 选择 in gpu
    service visbuf as VisBuffer 生成 in gpu
    service basepass as BasePass 材质着色 in gpu
    
    importer:R --> hzb:L
    hzb:B --> lod:T
    lod:B --> visbuf:T
    visbuf:B --> basepass:T

三、VisBuffer:Nanite 的秘密武器

传统 deferred / forward 渲染的流程大致是:CPU 提交 draw call → GPU 光栅化 → 写入 G-Buffer / 直接着色。Nanite 把前面两步改成了 GPU-driven。

3.1 Visibility Buffer

Nanite 使用一种叫 Visibility Buffer 的中间表示。第一步只记录每个像素属于哪个 cluster、哪个三角形、哪个重心坐标,不立即着色。第二步再根据这个信息做延迟的材质着色。

这样做有两个好处:

  • 材质解耦:几何处理不需要关心材质复杂度;
  • ** overdrawing 减少**:只有最终可见的三角形才会进入 BasePass。
sequenceDiagram
    participant C as CPU
    participant G as GPU
    participant V as Visibility Buffer
    participant B as BasePass
    C->>G: 提交 Nanite 绘制命令
    G->>V: 光栅化可见 Cluster,记录 (ClusterID, TriangleID, Barycentric)
    V->>B: 按像素解算材质与着色
    B-->>G: 最终图像

3.2 为什么是 GPU-driven

传统管线里,CPU 要决定画哪个 mesh、哪个 LOD、哪个 instance。当场景里有几万个静态物体时,CPU 端的开销会先于 GPU 成为瓶颈。Nanite 把 culling、LOD 选择、instance 合并都搬到 GPU,CPU 只需要给一个高层指令:「这些 Nanite 资源,渲染。」

用公式粗略描述渲染负载的转移:

TtraditionalTCPUdraw+TGPUgeometry+TGPUshadingT_{traditional} \approx T_{CPU}^{draw} + T_{GPU}^{geometry} + T_{GPU}^{shading}

T_{Nanite} \approx T_{CPU}^{minimal} + T_{GPU}^{culling} + T_{GPU}^{geometry}^{'} + T_{GPU}^{shading}^{'}

当物体数量 NN 增大时,TCPUdrawT_{CPU}^{draw} 线性增长,而 Nanite 的 CPU 开销几乎不变。这就是开放世界场景能从 Nanite 获得最大收益的原因。


四、集群与内存:天下没有免费的午餐

Nanite 不是魔法。它省掉了手工 LOD 的时间,但付出了其他代价。

4.1 磁盘与内存开销

启用 Nanite 后,单个网格的磁盘占用通常是普通网格的 2-3 倍。因为除了要存最终几何,还要存 cluster 层级、LOD 层次、BVH 加速结构等元数据。

内存方面,Nanite 会按需流送 cluster。但所有 instance 的根 cluster 必须常驻显存。如果你在一个开放世界里放了几万棵 Nanite 树,即使每棵树远处只显示几百个三角形,根 cluster 的总量也可能压垮 VRAM。

flowchart LR
    A[Disk 2-3x] --> B[DDC / Pak]
    B --> C[运行时按需流送]
    C --> D[VRAM 中的可见 Cluster]
    C --> E[常驻的根 Cluster]
    E --> F[Instance 数量过多会爆显存]

4.2 r.Nanite.MaxNodes 与 MaxTriangles

UE5.8 提供了几个关键控制台变量控制 Nanite 的渲染预算:

  • r.Nanite.MaxNodes:剔除遍历阶段最多处理多少 BVH 节点,默认约 200 万;
  • r.Nanite.MaxTriangles:每帧最多渲染多少 Nanite 三角形;
  • r.Nanite.MaxCandidateClusters:候选 cluster 池大小。

当场景过于复杂,这些缓冲区溢出时,引擎会弹警告:「Nanite node buffer overflow detected」。此时画面可能出现低精度几何或闪烁。解决方式通常是:

  • 提高 MaxNodes / MaxTriangles;
  • 减少 instance 数量;
  • 检查是否有异常高面数的小物体。
[/Script/Engine.RendererSettings]
r.Nanite.MaxNodes=4194304
r.Nanite.MaxTriangles=2000000

五、哪些资产该开 Nanite,哪些不该

Nanite 不是万能开关。根据官方文档和实际项目经验,可以总结出下面这张表:

资产类型是否适合 Nanite原因
英雄级环境物体(岩石、建筑、废墟)✅ 适合高面数、近距离、细节重要
Photogrammetry 扫描件✅ 适合省去 retopo 和手工 LOD
简单小道具(箱子、瓶子、碎石)⚠️ 可选收益小,远处直接变成几个像素
植被(带 Masked 材质)⚠️ 谨慎Masked bin 开销高,需测试
半透明物体(玻璃、水)❌ 不适合Nanite 不支持 translucent
骨骼网格角色❌ 默认不适合5.5+ 有实验性支持,但限制多
地形 Landscape✅ 适合UE5 原生支持 Landscape Nanite

5.1 一个常见错误

很多团队第一次用 Nanite 时,会把所有东西一股脑开启,包括小石子、草丛、远处的广告牌。结果性能没提升,内存还涨了。原因是:Nanite 的收益来自「减少远距离/overdraw 的几何开销」,但如果一个物体本身就很便宜,那预处理 cluster 的 overhead 反而会占主导。

老周后来定了一条内部规范:「面数低于两千、屏幕上常驻像素少于五十的静态物体,先不开 Nanite,等项目后期 profiling 再决定。」


六、Nanite 与 Virtual Shadow Maps 的共生关系

Nanite 的另一个重要搭档是 Virtual Shadow Maps(VSM)。传统阴影贴图对每个光源都要渲染一次阴影 pass,光源多了 shadow draw call 会爆炸。VSM 把阴影缓存成页表,只更新有变化的区域。

Nanite 网格投射 VSM 阴影时,可以利用同样的 cluster 流送机制,只渲染必要的三角形。这意味着:动态光源多了,Nanite + VSM 的组合比传统静态网格 + 级联阴影要便宜得多。

graph LR
    A[Nanite 几何] --> B[VisBuffer 主 Pass]
    A --> C[Virtual Shadow Maps]
    B --> D[最终图像]
    C --> D

这也是 MegaLights 能成真的基础之一。没有 VSM 和 Nanite,几百盏动态阴影灯根本不敢想。


七、5.8 的改进与仍需注意的坑

7.1 5.8 没有颠覆 Nanite,但打磨了细节

UE5.8 对 Nanite 的改动相对温和,主要是:

  • 更好的 cluster culling 效率;
  • 与 Mesh Terrain 的集成更顺畅;
  • 对 masked / foliage 材质的 bin 优化;
  • 调试视图和性能分析工具更完善。

7.2 仍然存在的限制

  • 不透明和 masked 材质才能用:translucent、decal、two-sided foliage 等 unsupported;
  • 不支持顶点动画:水面、布料变形不能用 Nanite;
  • 碰撞要单独处理:Nanite 几何不能直接用,需要生成 collision mesh;
  • 材质 ID 上限 64:超复杂模块化建筑可能触及;
  • 磁盘占用大:需要规划 DDC 共享和 Pak 分包。

7.3 一个故事:植被的教训

老周的团队曾经把所有树都开了 Nanite,以为可以告别 LOD。结果在森林里帧率反而下降。查了一周 profiler 才发现:树叶用了 masked 材质,每片叶子都被分到不同的 raster bin,导致 bin count 爆炸。

最后的方案是:

  • 树干和主要树枝用 Nanite;
  • 大量小叶片用传统 LOD + impostor;
  • 远景树林用 Billboard 或 HLOD。

Hybrid 方案往往比「全开」或「全关」更靠谱。


八、检查清单:给美术和程序的实操建议

美术侧

  • 英雄级静态几何尽量开 Nanite,省去手工 LOD;
  • 保持三角面分布合理,避免平坦区域过度细分;
  • UV 和顶点数据保持干净,只保留必要的通道;
  • 复杂资产注意材质 ID 不要超过 64;
  • 半透明部分拆出来用传统 mesh。

程序侧

  • stat Nanite 观察 cluster、triangle、instance 数量;
  • 监控 r.Nanite.MaxNodes 是否溢出;
  • 关注 VisBuffer 和 BasePass 各自的开销;
  • 开放世界项目重点控制 instance 总量,不是 triangle 总量;
  • 为小物件制定 Nanite 启用白名单。

九、Cluster 生成的幕后:导入时发生了什么

很多人在引擎里点一下「Enable Nanite」,以为只是改了个 bool。其实后台发生了一系列预处理。

9.1 导入流程

当你对一个 StaticMesh 勾选 Nanite 时,Unreal Header Tool 和导入管线会:

  1. 读取原始三角形数据;
  2. 把网格切分成最多 128 个三角形一组的 cluster;
  3. 为每组 cluster 生成简化版本,构建父子关系;
  4. 建立 BVH(Bounding Volume Hierarchy)加速结构;
  5. 把结果存进 DDC(Derived Data Cache),供运行时流送。
sequenceDiagram
    participant A as 美术导入 FBX
    participant B as StaticMesh 导入管线
    participant C as Cluster Builder
    participant D as BVH 构建
    participant E as DDC 缓存
    A->>B: 提交源网格
    B->>C: 切分 Cluster
    C->>C: 生成简化层级
    C->>D: 构建空间加速结构
    D->>E: 写入预处理数据
    E-->>B: 返回可用资产

这个预处理只在导入或修改时做一次。第一次导入高面数资产时,编辑器可能会卡几秒到几十秒,取决于面数和机器性能。

9.2 什么样的拓扑适合 Nanite

虽然 Nanite 能处理各种拓扑,但效率有高低之分。理想的输入是:

  • 三角形分布相对均匀;
  • 没有大量退化的细长条面;
  • UV 接缝合理,不会导致 cluster 边界过度破碎;
  • 顶点属性通道精简。

老周团队的经验是:从 ZBrush 直接导出的高模通常比从 CAD 软件转出来的模型更适合 Nanite。CAD 模型经常有大量共面、微小面和非法拓扑,cluster 生成效率低。


十、Raster Bin:材质复杂度才是新瓶颈

很多人以为 Nanite 之后就不用关心材质了。错。Nanite 只是把瓶颈从几何转移到了别处,其中最隐蔽的就是 raster bin

10.1 什么是 Raster Bin

Nanite 在光栅化阶段会把三角形按材质属性分组到不同的 bin 里。每个 bin 对应一种材质状态。状态越多,bin 越多,GPU 切换开销越大。常见的高 bin 场景包括:

  • 大量使用 masked / alpha test 的树叶、草丛;
  • 每个 mesh 使用大量不同材质 ID;
  • 复杂的两面渲染或自定义深度材质。

10.2 一个 profiling 案例

老周用 stat Nanite 看到一个植被场景的 raster bin 数飙到了三千多。作为参考,普通室内场景通常控制在几百以内。问题出在每片叶子都用了一个独立的 masked 材质实例。

优化方案:

  • 把相同植物的叶子合并到同一个材质实例;
  • 减少 alpha test 边缘的复杂度;
  • 远处树叶改用 opaque 或 dithered LOD。

优化后 bin 数降到六百,帧率回升了 18%。

10.3 常用 Profiling 命令

UE5.8 提供了一套完整的 Nanite 调试视图和统计命令:

stat Nanite           # 查看 Nanite 基础统计
stat GPU              # 查看 VisBuffer / BasePass 开销
r.Nanite.Visualize 1  # 打开可视化模式
r.Nanite.Visualize 2  # 查看 Cluster 分布
r.Nanite.Visualize 3  # 查看 Triangle 密度

在可视化模式下,你可以直观看到哪些区域 cluster 密度异常高,哪些资产在远处仍然保留了过多细节。


十一、与 Landscape 和 World Partition 的协同

UE5 从某个版本开始支持 Landscape Nanite。这让大面积地形也能享受自动 LOD 和 VSM 阴影的好处。对于开放世界项目,Nanite Landscape 配合 World Partition 可以显著减少远距离地形的渲染开销。

但这里有个容易忽略的点:Nanite Landscape 的 cluster 选择是基于屏幕空间的。如果相机拉得很高,整个地形会退化成很粗的三角形。这对策略游戏或俯视视角项目很友好,但对第一人称近距离场景需要更细粒度的高度图和材质层。

flowchart LR
    A[Nanite Landscape] --> B[World Partition 流送]
    B --> C[按相机距离加载地形 Cell]
    C --> D[Nanite Cluster 自动 LOD]
    D --> E[VSM 阴影]

十二、写在最后

很多人以为 Nanite 之后就不用关心材质了。错。Nanite 只是把瓶颈从几何转移到了别处,其中最隐蔽的就是 raster bin

9.1 什么是 Raster Bin

Nanite 在光栅化阶段会把三角形按材质属性分组到不同的 bin 里。每个 bin 对应一种材质状态。状态越多,bin 越多,GPU 切换开销越大。常见的高 bin 场景包括:

  • 大量使用 masked / alpha test 的树叶、草丛;
  • 每个 mesh 使用大量不同材质 ID;
  • 复杂的两面渲染或自定义深度材质。

9.2 一个 profiling 案例

老周用 stat Nanite 看到一个植被场景的 raster bin 数飙到了三千多。作为参考,普通室内场景通常控制在几百以内。问题出在每片叶子都用了一个独立的 masked 材质实例。

优化方案:

  • 把相同植物的叶子合并到同一个材质实例;
  • 减少 alpha test 边缘的复杂度;
  • 远处树叶改用 opaque 或 dithered LOD。

优化后 bin 数降到六百,帧率回升了 18%。

9.3 常用 Profiling 命令

UE5.8 提供了一套完整的 Nanite 调试视图和统计命令:

stat Nanite           # 查看 Nanite 基础统计
stat GPU              # 查看 VisBuffer / BasePass 开销
r.Nanite.Visualize 1  # 打开可视化模式
r.Nanite.Visualize 2  # 查看 Cluster 分布
r.Nanite.Visualize 3  # 查看 Triangle 密度

在可视化模式下,你可以直观看到哪些区域 cluster 密度异常高,哪些资产在远处仍然保留了过多细节。


十、与 Landscape 和 World Partition 的协同

UE5 从某个版本开始支持 Landscape Nanite。这让大面积地形也能享受自动 LOD 和 VSM 阴影的好处。对于开放世界项目,Nanite Landscape 配合 World Partition 可以显著减少远距离地形的渲染开销。

但这里有个容易忽略的点:Nanite Landscape 的 cluster 选择是基于屏幕空间的。如果相机拉得很高,整个地形会退化成很粗的三角形。这对策略游戏或俯视视角项目很友好,但对第一人称近距离场景需要更细粒度的高度图和材质层。

flowchart LR
    A[Nanite Landscape] --> B[World Partition 流送]
    B --> C[按相机距离加载地形 Cell]
    C --> D[Nanite Cluster 自动 LOD]
    D --> E[VSM 阴影]

十一、写在最后

Nanite 最打动人的地方,不是它能让一千万面的模型跑在主机上,而是它把「减面」这个枯燥、重复、容易出错的工作从美术身上卸了下来。老周现在做英雄级岩石,导入引擎后点一下 Enable Nanite,再去检查 cluster 分布和材质开销,省下的时间可以拿来雕更多细节。

但这套系统有它的脾气。它擅长处理复杂静态几何,不擅长处理半透明、变形、超低面物体。把它当成一把精确的手术刀,而不是一把乱砍的斧头,它才能真的帮到你。

工具的价值从来不在于它能做什么,而在于它让你不必做什么。Nanite 让开发者不必再当人肉 LOD 生成器,这已经够值了。剩下的时间,你可以去做更重要的事:设计更有趣的关卡,打磨更细腻的光照,或者干脆早点下班。


十三、给新人的一句话建议

如果你刚接触 Nanite,记住三条原则就够了:

  • 静态、不透明、面数高的物体,先开 Nanite;
  • 半透明、会变形、面数极低的物体,别开;
  • 开了之后一定要用 stat Nanitestat GPU 验证,别凭感觉。

剩下的事,交给时间。用得多了,你自然会培养出对 cluster 密度和 raster bin 的直觉。这跟骑车一样,摔过几次坑,就知道哪里该刹车。Nanite 已经帮你看清了很多坑,但方向盘还在你手里。开得快不快,路线选得对不对,最终还是取决于开车的人。工具再聪明,也替代不了判断力。希望这篇文章能帮你少摔几个坑,多省几天时间。


参考与延伸阅读