UE5.8 实战:HLOD 与 Nanite 的配合

📑 目录

导语:去年夏天做「远港 Online」的野外 demo,策划在 8km×8km 的大陆上摆了四万七千棵针叶树、两千多块废墟碎石、十七座瞭望塔。我第一次把镜头从海岸线一路拉到内陆高地,远景糊成一团色块,中景的树像纸片剪贴,近景的石头倒是纤毫毕现。帧率 38,显存爆了。问题不是某个资产做得不够好,而是远近物体的渲染策略各唱各的调。这一篇就聊 UE5.8 里把 HLOD 与 Nanite 拧成一股绳的做法。

场景:镜头从海岸拉到高地

想象一个开放世界的典型镜头。玩家站在海岸悬崖边,视线方向上有这几层东西:

  • 脚下三米:碎石、枯草、被海浪浸湿的木板,纹理要清晰到能看出木纹走向。
  • 三十米外:一片针叶林,单棵树的枝叶能被辨认,风吹过时树冠要整体晃动。
  • 三百米外:森林退成深绿色的起伏轮廓,树冠连成一片,细节不再重要。
  • 三公里外:只剩山脉剪影和天际线,玩家只需要知道那里有陆地。

这四层如果用同一套逻辑渲染,结果一定很惨。要么远景把几万个独立实例全画出来,Draw Call 和三角形数双双爆炸;要么近景用粗糙 LOD,玩家走到脸上还是马赛克。UE5.8 的解法是把四层拆给三套技术:远距离交给 HLOD 合并代理,中远距离交给 Nanite 自动细节,近距离骨骼网格或变形体交给传统 LOD。它们不是竞争关系,而是接力关系。

下面这张图是运行时渲染架构的概览。三个层级共享 World Partition 的流送结果,但各自维护不同的几何表示与切换规则。

flowchart TD

HLOD 是什么:把远景打包成一张皮

HLOD 的全称是 Hierarchical Level of Detail,直译是层级细节。名字容易让人误以为是另一种 LOD,其实它更像是 World Partition 的远景搭档。传统 LOD 是针对单个资产做几级减面版本;HLOD 是针对一片区域里的多个资产,把它们合并成一个或几个代理网格,再烘焙成一张合并贴图。

合并的好处非常直接。假设一片村落区域有 120 个独立静态网格:房子、木箱、水井、晾衣绳、杂草。每个网格都是一个 Draw Call,合起来就是 120 个。把它们整体烘焙成一个 HLOD 代理后,远处看只剩一两个 Draw Call。更关键的是,HLOD 代理可以用很低的顶点数覆盖很大的屏幕面积,远景本来就是糊的,玩家不会注意到细节丢失。

合并也有代价。代理网格是离线生成的,灯光与贴图已经烘焙死,运行时不能逐物体改动。这意味着 HLOD 层不适合会移动、会切换状态、会播放动画的对象。一扇可以被玩家打开的门、一盏会被法术打灭的灯、一棵会被风吹弯的树,如果被打包进 HLOD,运行时想单独控制它就难了。HLOD 最适合的是静态布景:岩石、建筑外墙、废弃车辆、墓地石碑。

HLOD 在 UE5.8 里默认与 World Partition 协同工作。World Partition 把世界切成格子,每个格子可以生成自己的 HLOD 代理。当玩家远离某个格子时,这个格子里的原始静态网格被卸载,只保留 HLOD 代理在内存和渲染管线里。靠近时再逐级加载原始网格。这个卸载过程是自动的,不需要关卡策划手动开关。

生成 HLOD 时需要指定几个关键参数:

  • HLOD 层级数:通常 2 到 3 层。第 0 层覆盖单格内部,第 1 层覆盖相邻几格的合并,第 2 层覆盖更大范围的远眺。
  • 合并距离阈值:多远开始把这一层当作有效代理。太远则代理过早出现,玩家能看出跳变;太近则失去合并收益。
  • 三角形预算:单个 HLOD 代理允许多少三角形。预算高则跳变不明显,但内存与绘制成本回升。
  • 贴图分辨率:合并后的贴图集大小。常见配置是 1024 到 2048,取决于代理在屏幕上的占比。

这些参数没有万能值,必须按项目的美术密度与目标平台反复测。主机项目和 PC 项目的预算可以差三倍。

HLOD 生成策略:不是全图一视同仁

全图用同一套 HLOD 参数通常是最差的选择。主城建筑密集,野外植被稀疏,海岸线有狭长地形,三者的最佳代理策略完全不同。UE5.8 的 HLOD 支持按区域覆盖不同的生成配置,常用做法是把世界拆成几类地貌,每类给一组参数。

下面是一条典型的生成管线流程。它从 World Partition 的格子出发,经过分类、减面、烘焙、验证四步。

flowchart TD
    A["World Partition 格子"] --> B{地貌分类}
    B -->|"主城"| C["密集建筑参数"]
    B -->|"野外"| D["稀疏植被参数"]
    B -->|"海岸"| E["狭长地形参数"]
    C --> F["网格合并"]
    D --> F
    E --> F
    F --> G["减面简化"]
    G --> H["贴图集烘焙"]
    H --> I["Lightmap 或 VT 准备"]
    I --> J["运行时距离验证"]
    J -->|"跳变明显"| K["回调参数"]
    J -->|"过渡自然"| L["锁定配置"]
    K --> C

地貌分类的依据可以很多:建筑密度、平均网格尺寸、材质 masked 比例、是否靠近玩家主要动线。主城建筑密度高,单个 HLOD 代理可以覆盖较小范围,但保留更多细节,因为玩家会在屋顶之间跳跃,能看清轮廓。野外植被稀疏,单个代理可以覆盖更大范围,顶点数压到极低,反正远处看就是一片绿。海岸线狭长,要防止代理把海岸线和内陆生硬地缝在一起,否则玩家沿海岸奔跑时会看到一条明显的分界线。

一个容易踩的坑是 HLOD 与 Nanite 的重叠。如果某个静态网格既开了 Nanite,又被纳入 HLOD 合并范围,离线生成 HLOD 时会拿到 Nanite 的高模源数据。Nanite 的高模通常有几百万三角形,直接合并进去会让 HLOD 代理重得离谱。正确做法是在生成 HLOD 前,对参与合并的源网格做一次非 Nanite 的 LOD0 抽取,或者让 HLOD 生成器使用资产的手工 LOD 链中合适的一级。UE5.8 的 HLOD 生成设置里有专门的 Nanite 处理选项,勾选后会自动抽取对应 LOD 作为合并输入。

另一个坑是透明与 masked 材质。HLOD 烘焙会把多个材质合并到一张贴图集上,masked 的叶片、栅栏、铁丝网在合并后边缘锯齿会被放大。这类材质要么在 HLOD 参数里单独排除,要么接受远景锯齿的现实。项目早期就要决定:远景的围栏是保留镂空剪影,还是退化成实心块。两种选择都可以,但不能让美术和程序各自拍脑袋。

HLOD 与 Nanite 的分工:谁管哪一段

Nanite 解决了中近景的细节问题。它把高模切成簇,运行时按屏幕像素精度决定画哪些簇,近处细节多,远处自动简化。听起来 Nanite 似乎也能管远景,为什么还要 HLOD?

因为 Nanite 的简化有极限。它的簇结构保留了原始拓扑关系,即使最粗的簇也有固定开销。当物体在屏幕上只有几个像素时,Nanite 仍要维护实例、簇组、流送请求,这些管理成本不会因为三角形少而消失。HLOD 则直接把成百上千个对象压成一张皮,管理成本与对象数量解耦。远景用 HLOD,本质上是用美术可控的失真换系统层面的轻量。

可以按屏幕占比把分工画成一条轴。设物体在屏幕上的投影面积占比为 AA

  • A<0.1%A < 0.1\% 时,HLOD 代理或更粗的 HLOD 层级接管。
  • 0.1%A<15%0.1\% \le A < 15\% 时,Nanite 自动选择簇密度。
  • A15%A \ge 15\% 且为静态硬表面时,Nanite 高模完整渲染。
  • A15%A \ge 15\% 且为变形体、半透明、骨骼网格时,传统 LOD 链接管。

这条轴不是引擎写死的,而是项目需要反复调整的距离带。UE5.8 里可以通过 HLOD 距离Nanite 剔除距离 两个参数来划定边界。Nanite 的剔除距离决定了一个 Nanite 实例在多远之后完全不再由 Nanite 渲染,转而交给 HLOD 或传统 LOD。这个距离通常放在 HLOD 切换距离之后,形成一段重叠缓冲区,避免玩家在边界处看到跳变。

下面用序列图展示运行时三者的切换顺序。玩家从远处骑马靠近一座废弃教堂,镜头始终注视着它。

sequenceDiagram
    participant Player as "玩家"
    participant HLOD as "HLOD 代理"
    participant Nanite as "Nanite 簇"
    participant LOD as "传统 LOD"

    Player->>HLOD: 距离 3km,教堂只占 2 像素
    HLOD->>Player: 显示岛屿级远景代理

    Player->>HLOD: 距离 800m,教堂占 0.1%
    HLOD->>Player: 切换到格子级 HLOD 代理

    Player->>Nanite: 距离 200m,教堂占 5%
    Nanite->>Player: 渲染 Nanite 简化簇

    Player->>Nanite: 距离 50m,教堂占 18%
    Nanite->>Player: 渲染完整高模簇

    Player->>LOD: 距离 20m,大门为骨骼门
    LOD->>Player: 单独绘制传统 LOD 链

注意序列里的最后一步。教堂主体是静态硬表面,可以走 Nanite;但大门是可互动的骨骼门,必须单独出来走传统 LOD。这种混合场景在大世界里非常常见,也是最容易出跳变的地方。大门从 Nanite 场景中被单独剔除后,会回到传统 LOD 链,策划和 TA 要确保它在切换距离上的视觉匹配。

远距离合并网格:HLOD 的主场

远距离的渲染目标只有一个:用最低成本给玩家一个可信的轮廓。玩家在三公里外不会数窗户,也不会检查屋顶瓦片。此时 HLOD 的合并网格要把大量独立对象压成少量 Draw Call,同时保持山脉、森林、建筑群的剪影可读。

合并网格的核心指标是 Draw Call 密度与屏幕像素数的比值。假设远景区域在屏幕上占 80000 像素,如果由 500 个独立实例绘制,每个实例平均只贡献 160 像素,但 CPU 却要发 500 次绘制命令。合并成一个 HLOD 代理后,CPU 侧只剩 1 到 2 次绘制命令,GPU 侧顶点数虽然可能略多,但远低于 500 次绘制调度的开销。

这个比值可以用一个简单的经验公式估算:

R=DPR = \frac{D}{P}

其中 RR 是 Draw Call 密度,DD 是绘制调用次数,PP 是屏幕像素数。远景优化要做的就是让 RR 降到可接受范围。对于 1080p 目标,远景 RR 建议控制在 10410^{-4} 以下,也就是每万像素不超过一次 Draw Call。4K 屏幕下可以稍微放宽,因为同样物理面积内像素更多,但独立实例的 CPU 开销不变。

HLOD 在远距离还有另一个好处:遮挡剔除的压力小。远景物体密集但细小,传统逐实例剔除的射线或光栅化测试会消耗大量 CPU。合并成代理后,剔除以代理为单位,代理数量比原始实例少两个数量级,剔除计算的性价比立刻提升。

不过合并代理也有视觉风险。最常见的是贴图走样。合并后的贴图集把多张材质压到同一张图里,mipmap 层级下降时,不同材质的边界容易互相渗透。UE5.8 允许在 HLOD 生成时设置贴图 padding 与边界扩展,减小渗透。另一个风险是法线信息丢失。合并减面会把小凹凸磨平,远景岩石变成塑料块。解决方法是在 HLOD 材质里保留一张宏观法线贴图,用低频率的法线变化模拟中频细节,而不是指望顶点本身。

中距离 Nanite:自动细节的甜点区

中距离是大世界镜头停留最久的区域。玩家骑马穿过森林、在废墟间搜刮、隔着河谷狙击敌人,这些场景的主体都在中距离。这个区间的特点是:物体在屏幕上有一定面积,细节可见但不需要无限精度;对象数量多,手工做 LOD 费时费力;玩家移动方向不可预测,切换距离会频繁被穿越。

Nanite 在这个区间几乎是量身定做的。它按像素决定绘制多少三角形,玩家靠近时自动增加细节,远离时自动减少。美术不需要为每块石头做四级 LOD,策划也不需要为每棵树调切换距离。更重要的,Nanite 的过渡是连续的,而不是 LOD 那种整数级跳变。玩家骑马从 100 米靠近到 30 米,石头的细节是平滑增加的,不会在某一个距离突然换模型。

Nanite 的连续过渡有一个数学基础:每个簇的屏幕空间误差被量化成一个阈值,运行时只选择误差小于像素尺寸 pp 的簇。这个关系可以写成:

ϵscreen=ϵworldhd<p\epsilon_{screen} = \frac{\epsilon_{world} \cdot h}{d} < p

ϵworld\epsilon_{world} 是簇在世界空间的几何误差,hh 是屏幕高度,dd 是物体到相机的距离。只要左边小于像素尺寸 pp,这个簇就可以被接受。这意味着同样一个簇,离得近时被拒绝,离得远时被接受,过渡自然由距离决定。

中距离使用 Nanite 时,最需要关注的是材质复杂度,而不是三角形数。Nanite 把三角形管理自动化了,但每个像素上的着色成本不会少。masked 叶片、复杂多层材质、高频法线贴图在中距离会吃掉大量 GPU 时间。项目里常见的一个误区是:开了 Nanite 就觉得可以无限堆高模,结果中距离的材质成本让帧率腰斩。

在「远港 Online」的野外 demo 里,我们中距离的主力资产是针叶树和岩石。针叶树用 Nanite,但材质做了专门简化:取消内部次表面散射层,把叶片从双面减为单面 masked,法线贴图 mipmap 偏移一档。岩石则直接上高模扫描件,因为硬表面是 Nanite 最擅长的类型。这些选择没有写在任何文档里,是测出来的:把同类资产单独放进空场景,看 GPU 时间随距离的变化曲线,曲线拐点就是问题所在。

近距离完整细节:Nanite 与传统 LOD 的交界

近距离是玩家最能挑出毛病的地方。角色站在一堵墙前面,砖缝、锈迹、破损的水泥块都要经得起看。这个区间里,Nanite 负责静态硬表面,传统 LOD 负责变形体和半透明,两者必须无缝衔接。

静态硬表面在近距离用 Nanite 高模是顺理成章的。摄影测量扫描的废墟、ZBrush 高模的雕像、 millions of triangles 的机械残骸,都可以直接丢进场景。美术的产能被释放出来,不需要再花时间手动减面。但这里有一个操作细节:Nanite 资产在近距离的三角形密度可能远超屏幕像素,引擎会自动裁掉不可见的簇,所以不会真的画几百万三角形。真正要担心的是材质 overdraw 与贴图采样。近距离下玩家会绕着物体转,贴图各向异性过滤与 mipmap 选择必须正确,否则会出现闪烁或模糊。

近距离的变形体传统 LOD 链仍然不可替代。角色、载具、可互动门、风场植被、布料,这些资产在 UE5.8 里主要还是走骨骼网格或静态网格 LOD 链。Nanite 对变形体的支持在逐步扩展,但项目上线时不能赌实验性功能。我的做法是:把变形体从 Nanite 场景中明确排除,单独建立 LOD 链,并在近距离与 Nanite 场景做光影匹配。

光影匹配是一个容易被忽略的坑。Nanite 使用虚拟阴影贴图 VSM,传统 LOD 链可能使用级联阴影贴图 CSM。如果一扇门的传统 LOD 投下的是 CSM 阴影,而旁边 Nanite 墙壁投下的是 VSM 阴影,两者在交界处会有精度差异。解决方法是统一阴影方案,或者在门的周围放一个静态阴影投射体,让它与 Nanite 场景共享 VSM。

近距离还有一个细节是碰撞体。Nanite 高模通常不直接用于碰撞,碰撞体是单独的低模。玩家在近距离观察到的视觉细节与物理反馈可能不一致,比如一块凸出的碎石 visually 在那里,但玩家能穿过去。策划需要在碰撞体上标注哪些视觉凸出需要物理阻挡,哪些可以忽略。这个工作无法自动化,只能人工走查。

HLOD 颜色视图解读:用眼睛看账单

UE5.8 编辑器提供了 HLOD 可视化模式,用颜色告诉你当前屏幕上哪些对象走了 HLOD,哪些走了 Nanite,哪些还在传统 LOD。学会读这个颜色视图,比看任何数字都直观。

常用视图模式包括:

  • HLOD 颜色:按 HLOD 层级染色。远景岛屿级代理用一种颜色,中景格子级代理用另一种颜色,未进 HLOD 的原始网格用第三种颜色。
  • Nanite 三角形密度:用热力图显示屏幕上每块区域的三角形密度。高密度区域亮红,低密度区域暗蓝。
  • Draw Call 覆盖:用颜色标注每个 Draw Call 覆盖的屏幕范围,帮助发现小面积高调用区域。

读 HLOD 颜色视图时,重点看两个东西:边界和孤岛。

边界是指 HLOD 层级之间的过渡带。理想情况下,过渡带应该连续平滑,颜色渐变。如果看到一条锐利的色带横扫屏幕,说明两层 HLOD 的切换距离太近,玩家靠近时会发生明显跳变。解决方法是拉大两层之间的距离阈值,或者在中间插入一个过渡层。

孤岛是指一小块区域的颜色与周围明显不同。比如一大片绿色 HLOD 代理里突然出现一块红色原始网格。这种情况通常说明那块区域没有被正确纳入 HLOD 生成,可能是因为资产设置了 Never StreamExclude from HLOD。孤岛会单独产生 Draw Call,在远景里非常昂贵,必须排查。

下面这张图展示了不同距离带上的颜色理想分布。横轴是距离,纵轴是视觉占比,颜色块代表各技术的覆盖范围。

graph LR
    A["近距离
0-30m"] -->|"Nanite 高模 + 传统 LOD"| B["中距离
30-300m"] B -->|"Nanite 简化簇"| C["远距离
300m-2km"] C -->|"HLOD 格子级代理"| D["超远距
2km+"] D -->|"HLOD 岛屿级代理"| E["天际线"] style A fill:#f9d5bb style B fill:#b8e0d2 style C fill:#a8c5e2 style D fill:#c9b8e8 style E fill:#e8b8c0

颜色视图还能帮你验证 Nanite 与 HLOD 的分工是否合理。如果你发现中距离大面积亮起 Nanite 高密度红区,说明中距离资产要么太复杂,要么距离阈值设得太近。如果你发现远景还有大量未进 HLOD 的原始网格,说明 HLOD 生成参数或 World Partition 设置有问题。

性能优化:三组数字与一条纪律

聊完分工,最后落到数字上。大世界性能优化不是一次性任务,而是一组持续监控的指标。与 HLOD / Nanite 配合最相关的指标有三组。

第一组是 Draw Call 与三角形数的距离分布。用 stat scenerendering 或 Unreal Insights 抓取一帧,按距离桶拆分 Draw Call 和三角形数。理想分布是:远景 Draw Call 极低、三角形数中等;中距离 Draw Call 中等、三角形数由 Nanite 自动压下;近距离 Draw Call 与三角形数都高,但覆盖屏幕面积也大。如果某距离桶的 Draw Call 或三角形数异常高,就去颜色视图里找对应颜色块。

第二组是 Nanite 开销。关键命令是 stat nanite,看簇数、实例数、光栅时间。中距离场景下,实例数过多通常比三角形数过多更要命。实例数多意味着 CPU 侧管理与流送压力大,而 Nanite 的强项是压三角形,不是压实例。如果实例数爆表,考虑用植被实例化工具或把远处小对象推进 HLOD。

第三组是 HLOD 代理的内存与显存占用。HLOD 代理是离线生成的,生成后会作为独立资产存到磁盘。大世界全图 HLOD 加起来可能占用数 GB 磁盘空间,运行时显存占用取决于同时激活的 HLOD 层级数。监控工具可以用 stat memory 配合内存分析器。如果发现 HLOD 纹理池持续增长,检查贴图集分辨率是否过高,以及是否有重复生成的代理。

优化纪律只有一条:先定位,再动手。不要看到帧率低就全局降 Nanite 精度或全局砍 HLOD 距离。这两套系统都有大量可调参数,但参数之间互相影响。一个常见错误是把 Nanite 剔除距离拉远,结果远景本该由 HLOD 代理的对象开始用 Nanite 渲染,实例管理开销反而上升。另一个错误是把 HLOD 三角形预算压太低,导致中远距离出现明显跳变,玩家投诉画面缩水。

我的习惯是做一个 参数影响矩阵,把每个可调参数和它影响的指标列出来。调参数时一次只动一个,跑同一条路线三次取中位数。下面是一个简化示例,项目里可以扩展成完整表格:

; 大世界 HLOD / Nanite 调参速查
; 调完之后按同一路线跑三次,取 stat unit 中位数

r.Nanite.MaxPixelsPerEdge 0.5       ; Nanite 簇密度,值越小三角形越多
r.Nanite.MaxStreamingMips 14        ; 流送 mipmap 上限,控制显存
r.HLOD.Distance 10000.0             ; HLOD 切换距离,单位 cm
r.HLOD.MaximumLevel 2               ; 最高 HLOD 层级,0=最近,2=最远
stat Nanite                         ; 看簇/实例/光栅时间
stat SceneRendering                 ; 看 Draw Call 与三角形总数
stat Memory                         ; 看显存与内存占用

这段命令一共九行,但已经覆盖了日常调优的入口。团队里每个人都要能解释自己改的参数在影响什么,而不是跟着网上某篇帖子抄数字。

写在最后:配合比单项技术更重要

HLOD 与 Nanite 都是 UE5.8 大世界渲染的重要工具,但真正的收益来自它们之间的配合。HLOD 把远景压成皮,Nanite 把中近景细节自动化,传统 LOD 兜底变形体。三者之间的边界由距离、屏幕占比、对象类型共同决定,不是引擎替你一刀切,而是项目自己反复测出来的。

「远港 Online」那个 demo 最后是怎么过的?我们把世界按地貌分了四类参数,HLOD 做了三级,Nanite 保留给硬表面和静态树,草和可互动门走传统 LOD。调了大约两周,RTX 3070 / 1080p 下野外场景稳在 60fps,显存占用从 9.2GB 降到 6.8GB。没有用到任何黑科技,只是把远近物体的分工理清楚了。

下次评审会上如果有人问:"这个项目大世界优化做得怎么样?" 我希望你能回答的不是某个参数值,而是一张清晰的距离带图:远景是什么、中景是什么、近景是什么,每种对象为什么归这一类。能画出这张图,优化就成功了一半。