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,不需要统一切换。

flowchart TD
    subgraph gpu ["GPU"]
        hzb["HZB 遮挡剔除"]
        lod["Cluster LOD 选择"]
        visbuf["VisBuffer 生成"]
        basepass["BasePass 材质着色"]
    end
    subgraph cpu ["CPU"]
        importer["导入时构建 Cluster DAG"]
    end
    importer --> hzb
    hzb --> lod
    lod --> visbuf
    visbuf --> basepass

三、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 已经帮你看清了很多坑,但方向盘还在你手里。开得快不快,路线选得对不对,最终还是取决于开车的人。工具再聪明,也替代不了判断力。希望这篇文章能帮你少摔几个坑,多省几天时间。


参考与延伸阅读