ARTICLE · 998541
Nanite 地形源码拆解:UE5.8 如何把 8129² 顶点的大地图,切成 64 张 Nanite 网格
你有没有遇到过——项目做了张 8km×8km 的开放世界地图,导入 Landscape 之后,远看还行,一拉近镜头,地形边缘哗啦啦地跳 LOD,草和石头贴图也跟着闪?更糟的是你打开 Nanite 地形开关想解决性能,结果材质里的图层混合(Layer Blend)直接不显示了。
这不是你的配置问题。是 Landscape 这套"高度图 + 组件 + 子分区"的老架构,和 UE5 的 Nanite 虚拟化几何之间,隔着一层你平时看不到的翻译管线。今天我们把 UE5.8 源码里这条管线扒开,看清楚大地图到底被切成了几刀,每一刀在 GPU 上发生了什么。
先看数据模型。UE5.8 的 LandscapeProxy.h 里,决定地形怎么切的三兄弟就躺在 LandscapeProxy.h:924 这一带:
// LandscapeProxy.h:924 数据模型三兄弟int32 ComponentSizeQuads; // 每个组件的 quad 总数int32 SubsectionSizeQuads; // 子分区 quad 数,+1 必须是 2 的幂int32 NumSubsections; // X/Y 轴上的子分区数这三行就是整个 Landscape 的骨架。我们写地形的时候天天看到"Component(组件)""Section(子分区)""Quad(四边形)"这几个词,但它们的层级关系决定了后面所有性能和 LOD 行为。官方《Landscape Technical Guide》给的标准尺寸表是这样的:
注意第一个细节:组件是渲染、可见性剔除、碰撞的基本单位,而子分区才是 LOD 计算的基本单位。这就解释了为什么有些地图明明组件不多,LOD 却跳得厉害——LOD 的粒度是按子分区来的,不是按组件。
第二个细节藏在 LandscapeComponent.h:227。高度数据不是存在一个数组里,而是按 LOD 分开存,而且用的是 16 位无符号整数:
// LandscapeComponent.h:227 高度数据按 LOD 存 16-bitTMap<int32, TArray<uint16>> HeightMipData;16-bit 意味着什么?高度值只有 65536 个档位。一张垂直落差 5000 米的地图,每个档位代表 5000/65536 ≈ 7.6 厘米的精度。这也是为什么超大地形会看到"台阶感"——不是渲染问题,是 16-bit 高度图的量化误差。LOD 1 以上的高度存在这个 TMap 的 mip 里,跟纹理 mipmap 一个思路:越远的 LOD 用越粗的采样。
现在说重点:Nanite 地形。UE5.8 里 bEnableNanite 这个开关,注释写得极其诚实——LandscapeProxy.h:489:
// LandscapeProxy.h:489// Use Nanite to render landscape as a mesh// on supported platforms.bool bEnableNanite = false;翻译一下:Nanite 地形不是"让 Landscape 原生支持 Nanite",而是把整张高度图地形,悄悄转成一张 Static Mesh,再交给 Nanite 管线去渲染。这就是为什么开着 Nanite 地形时,图层混合材质会失效——你的 Layer Blend 是高度图管线的概念,转到 Mesh 之后就丢了这套语义。
那大地图到底被切成了几刀?答案在 LandscapeProxy.h:1089:
// LandscapeProxy.h:1089 Nanite 切块上限staticconstexpr int32 NaniteComponentMaxSide = 8;staticconstexpr int32 NaniteMaxComponents = NaniteComponentMaxSide * NaniteComponentMaxSide; // 64每张 Nanite 网格最多覆盖 8×8=64 个 Landscape 组件。回到最上面那张 1024 组件的表——1024/64=16,也就是说一张 8129² 顶点的地图,会被切成 16 张 Nanite 网格。每张网格负责 64 个组件的高度图数据,独立构建、独立缓存。
为什么要切?因为 Nanite 构建是有代价的。构建过程在 LandscapeNaniteComponent.cpp 里是一套异步管线,核心步骤是把高度图导出成 FMeshDescription,再交给 UStaticMesh::BatchBuild。看这段缓存逻辑——LandscapeNaniteComponent.cpp:286:
// LandscapeNaniteComponent.cpp:286 DDC 缓存键staticconstchar* MeshExportVersion ="b395d787-a7f0-1be9-244a-0c8689c8b7a6";FSHA1 Hasher;Hasher.Update(reinterpret_cast<const uint8*>(&ProxyContentId),sizeof(FGuid));这段代码做的事:每次导出 Nanite 地形网格,都用 ProxyContentId(地形的版本 GUID)加一个硬编码的导出版本号,算出一个 SHA1 哈希,作为 DDC(派生数据缓存)的 key。目的是——同一张地形只要没改过,就直接从缓存读网格描述,不重新导出。这就是"编辑一次地形,Nanite 网格只在第一次卡一下,之后秒加载"的底层原因。
再看真正干活的导出那步,LandscapeNaniteComponent.cpp:398:
// LandscapeNaniteComponent.cpp:398 导出 + 缓存bSuccess = LandscapeWeakRef->ExportToRawMeshDataCopy( ExportParams, *NaniteMeshDescription, AsyncBuildData.Get());// 成功则序列化写入 DDC,下次直接读缓存把高度图转成 FMeshDescription(UE 的通用网格描述结构),再用 FMeshDescriptionHelper::SetupRenderMeshDescription 清理优化,最后喂给 Nanite 构建器。整条链是"高度图 → 网格描述 → DDC 缓存 → Nanite 资源"。
这里有个关键的反直觉结论:Nanite 地形并不总是更快。Epic 官方的 5.8 Mesh Terrain 直播里,社区实测 16km×16km 的网格瓦片地形,性能和 Nanite Landscape 打平;而且 Nanite 地形在大面积场景下,如果没有大量大型遮挡体,反而可能比传统地形更慢。原因是传统 Landscape 的子分区 LOD 是按距离渐进降级的,而 Nanite 是逐像素的 cluster 剔除——两者优化的对象不一样,一个是"远距离整体降面",一个是"像素级看不见就不画"。
那什么时候该开?给一个能落地的判断:如果你开了 Nanite 地形,是为了让地形和周围的高模岩石、建筑共用一套 Nanite 剔除管线,且场景里有大量遮挡物,那值得;如果只是想做张空旷的大平原,传统高度图地形加 World Partition 的 HLOD 反而更省。
配置层面,开启很简单:选中 Landscape,在 Nanite 分类勾上 Enable Nanite。构建过程是异步的,源码里专门做了卡死检测——LandscapeNaniteComponent.cpp:51 的 CheckForStallAndWarn 会监控构建耗时,超时打警告日志。控制超时阈值的 CVar 是 landscape.Nanite.StallDetectionTimeout。如果你构建时看到 "Nanite Build Task ... is taking a long time" 的警告,调大这个值或检查 DDC 是否正常。
最后补一个实操:如果你想自己读一张 16-bit 高度图验证精度,Python 三行就能拆:
import structw, h = 8129, 8129withopen("heightmap.raw", "rb") as f: raw = f.read(w * h * 2)# 小端 16-bit 无符号,正好对上 uint16 档位heights = struct.unpack(f"<{w*h}H", raw)读完你会发现 heights 里全是 0~65535 的整数,这就是 Landscape 高度图的全部真相——一块巨大的 16-bit 灰度图,被组件和子分区切块,再按需被 LOD 抽稀或喂给 Nanite。
延伸一层:UE5.8 同期还有个实验性的 Mesh Terrain,它能表示山洞、悬崖悬挑、拱门这类高度图根本表达不了的三维地形。和 Nanite 地形的区别在于——Nanite 地形是"高度图转网格"的适配层,Mesh Terrain 是"原生网格地形"的正统路线。未来大世界地形大概率会从"高度图"迁移到"Mesh Terrain",但眼下高度图 + Nanite 的这套兼容层,仍是最稳的过渡方案。
我们把 UE5.8 源码里 Landscape 这条线完整走了一遍:16-bit 高度图是数据的真相,Component/Section/Quad 是切块的三级粒度,而 Nanite 地形本质上是一次"高度图 → 网格"的异步翻译,8×8 的切块上限决定了大地图会被切成几张独立缓存、独立构建的 Nanite 网格。
我每周拆一个 UE5 底层系统,从源码级讲到能跑的代码,不为流量,就想把技术讲透。觉得有用,点个"在看"让更多同行看到,关注后回复「UE5」获取我整理的 UE5 源码阅读路线图。
下一篇想拆 Mesh Terrain 还是 World Partition 的 HLOD?评论区点名,我优先安排。