把一台2000万三角面的扫描车模型拖进UE5关卡视口的瞬间,我其实做好了编辑器卡死的心理准备。结果它没有崩,视口转动顺滑,帧时间也没有出现离谱的尖刺。这个反差让我后来花了很长时间去研究UE5 Nanite渲染流程,不是把它当成一个“自动LOD开关”,而是去理解它为什么能让高模直接进入生产场景,以及哪些时候高模又会反过来成为压垮帧率的元凶。
这篇文章是我在多个项目里实际使用Nanite之后的总结,会覆盖底层数据结构、GPU调度方式、光栅化路径、与下游阴影和光追的耦合,以及落地时真正遇到的坑。适合技术美术、客户端图形程序员,还有那些正在被美术同事追问“高模能不能直接进引擎”的各位。
1. 先搞清楚Nanite在解决什么:传统网格LOD链条的拐点
很多人在评估Nanite时,第一反应是去找性能对比数据,但我建议先理解它解决的问题。如果你只把它当成一个更聪明的LOD系统,后面遇到的调度逻辑、内存策略、材质限制都会让你一头雾水,因为这些设计动机全部来自传统渲染的痛点。
1.1 传统LOD管线:一个“多模型堆叠”的妥协方案
在Nanite出现之前,把高精度资产放进游戏场景基本只有一条路:制作多级LOD。美术在DCC工具里把高模从几百万面依次减到几十万、几万、几千,每一级都要单独检查轮廓、法线、UV、遮罩,防止切换时出现“跳变”和穿插闪烁。一个资产花两三天做LOD是常见的事,如果是数千个资产的开放场景,LOD制作成本会直接变成项目管理上的黑洞。
但真正的瓶颈不只是美术成本。传统渲染管线的每个Mesh实例,都要在CPU侧提交Draw Call,渲染线程需要处理一大堆状态切换。LOD可以降低三角形数,却很难同时压低CPU的Draw Call开销。同一个高模在屏幕上只占了50个像素,你为了细节仍然得提交它的完整几何体,因为当时引擎并不知道“哪些三角形真的可见”。
于是传统方案只能在美术工作量、CPU开销、GPU开销三者之间做很痛苦的平衡。做LOD少了,远处帧率崩;做LOD多了,资产制作周期长到版本排期爆炸。
1.2 “虚拟化几何”思路:让三角形数量不再是第一瓶颈
Nanite换了一个思路。它不把几何体当一个完整的Mesh来调度,而是先把模型切成许多小块,运行时只把“当前视角下需要的那些小块”加载到GPU,并且由GPU自己去决定每个像素附近需要多细的几何。它借鉴了虚拟纹理的工作方式:像纹理分页那样管理几何数据,因此官方定位是虚拟化几何体系统。
我自己的理解是,Nanite把传统渲染中的“三角形数量成本”从线性函数拉平了。场景总三角形数可以到几十亿,但只要当前帧实际投影到屏幕上的粒度合理,GPU就能接受。注意这句话有个前提:它只是让三角形数量不再是第一瓶颈,并不代表Nanite没有成本。光栅化后的像素覆盖、Overdraw、流送带宽和内存占用,仍然是你需要关心的指标。
2. 看懂渲染流程必先看懂数据结构:簇、DAG与虚拟页
Nanite渲染流程是一个GPU Driven Pipeline,但驱动它的数据结构很有特点。不了解这些结构,你读代码和文档时会非常痛苦。这里我挑三个最核心的讲:簇(Cluster)、有向无环图(DAG)、虚拟页。
2.1 簇(Cluster):以128个三角形为单位的几何粒度
Nanite在离线处理阶段会把静态网格切成大量小的三角形簇,官方默认每个簇最多128个三角形。为什么是128这个数字?因为它要兼顾剔除粒度和索引管理的效率。簇太大,剔除就不够精细;簇太小,元数据开销会盖过几何本身。
每个簇都保存了自己的边界数据,比如包围盒、法线锥等。这让GPU在做裁剪时可以非常粗粒度地跳过一整个簇,而不是一个三角形一个三角形地判断。
从美术视角来看,你最终提交给Nanite的是一个巨大的高模网格。但到引擎内部,这个网格已经被重组为成千上万个独立的小几何包。UE5的Nanite网格资产仍然可以被编辑、被引用、被实例化,但它实际的几何数据已经进入了Nanite专用的资源体系,而不是传统静态网格的RenderData路径。
2.2 DAG:LOD从“离散”走向“连续”
簇不是孤立存在的。每个父簇在某些方向上有更高精度的子簇,这些父子关系构成一个有向无环图。父簇代表粗略几何,子簇代表局部更精细的几何。渲染时引擎沿着DAG遍历,根据屏幕空间误差决定要不要下探到子簇。
这里的LOD是连续且局部的。传统LOD是整个模型从“高精度版本”切换到“低精度版本”,切换瞬间可能出现明显跳动。Nanite的LOD选择以簇为粒度,同一模型近处部分使用高精度子簇,远处部分保留粗粒度父簇,中间是平滑转化的。你感知不到“切换”,只会在特定距离上看到细节密度变化。
DAG结构也使得实例共享非常高效。多个实例只要引用同一个Nanite网格资源,它们共享同一套簇和DAG,GPU可以一次性处理大量实例,不会为每个实例复制几何数据。
2.3 虚拟页与流送:像虚拟纹理一样管几何数据
Nanite的高模数据不会全部驻留显存。和虚拟纹理类似,几何数据被拆到一个个页(Page)里,每个页包含若干簇。运行时根据当前相机的可见性和误差度量,决定哪些页需要驻留,哪些可以先留在磁盘或内存中。
这带来一个显而易见的好处:一个超大规模场景,即使资产总三角形数高得吓人,内存预算仍然可控。但也要付出代价:页流送需要预测性加载。如果相机瞬间瞬移,或场景复杂度突然飙升,Nanite可能来不及加载需要的页,你会看到几何体短暂以较粗的形态出现,然后在一两帧内细化。这个问题我在后面的“实战坑位”里会展开讲。
3. 一帧内Nanite的完整执行路径:从实例到像素
理解了底层结构,现在可以完整地看一帧里Nanite做了什么。为了容易记忆,我把整个流程拆成四个阶段:实例收集与粗剔除、DAG遍历与簇级淘汰、光栅化、可见性缓冲生成与材质解析。
3.1 实例收集与粗粒度剔除
Nanite不是对场景里每个Mesh分别Draw Call。CPU每帧会把所有可见的Nanite实例收集起来,按网格资源、材质、变换等维度分组成批。这一步的核心目的是把成千上万个实例聚合为数量极少的GPU驱动批次,减少CPU提交压力。
粗剔除阶段使用的是比较传统的手段:视锥剔除、距离剔除、包围球剔除。CPU会先剔除明显不可见的实例,GPU随后再做更细的裁剪。值得注意的是Nanite支持持久化遮挡剔除,它会把上一帧生成的遮挡结果保留下来,用于降低当前帧的无效可见性测试。如果你的场景里有大量被建筑遮挡的高模资产,这个机制带来的节省非常明显。
3.2 DAG遍历:边剔除边选LOD
进入GPU阶段后,Nanite会沿着DAG做分层遍历。每个簇节点都要做两类判断:
- 是否可见:通过包围盒与视锥求交,加上法线锥背面剔除,快速丢弃不可见区域。
- 是否够精细:根据屏幕空间误差阈值判断当前节点的几何精度是否满足像素需求。如果还不满足,就继续下探到子簇;如果已经满足,就保留当前簇并送入光栅化候选列表。
这里有个容易误解的地方:LOD选择和剔除是在同一趟遍历里完成的。所以剔除过程不只是“丢弃不可见三角形”,还在决定“保留哪些精度的三角形”。这种连续性的好处是,同一个模型在画面中心区域可能使用顶级精度的子簇,在屏幕边缘区域则使用粗粒度父簇,一切都由误差阈值驱动。
3.3 光栅化方式的演进:从VS栅格化到Compute栅格化
Nanite的光栅化是历史上变化最大的部分。早期版本以Vertex Shader驱动的软光栅为主,后来又加入了硬件光栅模式;到了较新的引擎版本,默认路径已经迁移到基于Compute的多边形采样方式。
先说传统硬件光栅化。它适合处理大规模的三角形流,但Nanite的三角形数量高到普通Draw Call无法承载,所以要依赖GPU的自适应调度。在前几代引擎版本里,Nanite把候选簇分成若干批次,通过可编程的裁剪和分类阶段,把可能可见的三角形再送进光栅器。
而Compute多边形采样则更进一步:三角形不再依赖传统光栅化硬件的固定流程,而是以计算着色器的方式调度,一个或多个线程组负责一组三角形,先把三角形覆盖信息写入屏幕空间的Raster Bin,再用软件容积栅格化展开。采用这种方式后,Nanite可以更灵活地控制每个三角形的覆盖率评估,也为后续的MSAA支持提供了基础。
从项目实践看,老版本中光栅化质量受限于硬件光栅化精度,某些边缘会闪烁,新版本的Compute路径显著改善了这种情况。但代价是对硬件特性要求更高,所以你会看到Nanite的启用条件越来越强调必须支持较新的图形API和Shader Model。
3.4 Visibility Buffer与GBuffer通道的衔接
Nanite光栅化阶段不会立刻算颜色。它先往一个叫做Visibility Buffer的缓冲里写入每个像素对应的可见三角形信息,包括实例ID、簇ID、三角形ID等。这个缓冲相当于一个“像素到三角形的索引表”。
等到深度最先的三角形确定后,再执行材质解析Pass。每个像素根据Visibility Buffer里的三角形索引,从簇的Vertex Buffer中取出顶点属性,插值出法线、UV、切线等,然后执行材质着色,写入GBuffer。
这种设计的价值在于:传统延迟渲染里,每个三角形即使被完全遮挡,也要走一遍像素着色流程;而Nanite通过先深度后解析的方式,把材质计算集中到真正可见的像素上。你可以把Visibility Buffer理解成“只有最终赢家的名单”,后续的所有着色都发生在赢家身上,不再为被遮挡的三角形浪费算力。
4. 下游链路变化:阴影、光追和材质解析
Nanite不是孤立的渲染通道。它接管了场景几何的可见性计算后,下游的阴影系统、光追加速结构和材质解析都跟着发生了变化。
4.1 Visibility Buffer解析与材质分类
在Nanite的材质解析阶段,引擎拿到每个像素的三角形索引后,需要根据材质类型做分发。如果像素对应的材质属于支持Nanite的材质域,解析Pass会用该材质的Shader执行GBuffer输出;如果是不支持的材质,往往会回退到传统渲染路径,这也是为什么一个场景里如果混用了Nanite网格和大量不支持Nanite的材质,性能预期会变得复杂。
Nanite材质节点的限制不是技术不行,而是因为Visibility Buffer丢失了逐三角形连续着色信息。传统Pass可以依赖像素内的导数做纹理过滤,Nanite解析时需要通过专门的机制保证贴图过滤质量。实际项目中,我发现把细节纹理和法线整合到材质里通常问题不大,但复杂的不透明混合、世界坐标位置偏移这类操作就要格外谨慎。
4.2 Virtual Shadow Map与Nanite的匹配
Nanite场景如果用传统阴影贴图,阴影分辨率会非常感人。巨大的几何体往往需要超高分辨率阴影才能保持边缘锐利,而传统阴影贴图整张分配的效率太低了。
Virtual Shadow Map解决了这个问题:它把阴影贴图也虚拟化,只在需要的地方分配并更新页。Nanite本身就是GPU驱动的几何系统,两者配合时,引擎可以根据光源影响范围动态决定哪些阴影页需要渲染,哪些可以保留缓存。我在实测中观察到的效果是:角色近距离的阴影清晰度提升明显,同时阴影更新开销被控制在很小的区域内。
4.3 光线追踪场景中的Nanite BVH重建
如果你开了光追,Nanite几何体不会直接以三角形流送给光追硬件,而是需要先构建加速结构。Nanite在GPU侧维护一份虚拟几何的边界层次结构,转成光追所需的加速结构后,再由光追管线查询。
这个过程的代价并不低,尤其是场景里Nanite资产特别多时,BVH重建会成为单独的GPU开销项。如果你的目标硬件是高端PC或主机,配合硬件光追加速还能接受;如果目标平台较老,我会建议优先使用Virtual Shadow Map而非全屏光追。有些项目在实际测试中关闭了Nanite网格的光追可见性,改用屏幕空间效果去模拟反射和阴影,性能立刻改善一个档次。
5. 项目落地:开启Nanite、内容校验与常见坑位
理论讲多了容易飘,回到地面看实际落地。这里列出的经验来自多个项目的实测,不一定适用于所有团队,但至少能让你少踩几个我踩过的坑。
5.1 确认引擎版本和工程环境
先说版本。UE5.0刚出时Nanite限制很多,比如不支持某些材质操作,光栅化路径也还在迭代;到UE5.1和UE5.2逐步完善了Virtual Shadow Map、光追配合和更多可视化调试工具;UE5.4之后默认的光栅化方式改为新的Compute多边形采样,对硬件特性要求更高。
如果你的项目还停留在UE5.0,别急着大规模启用Nanite。先检查目标机器的显卡驱动和API支持,Nanite要求支持较新的图形API和Shader Model,在较老的显卡上会直接走回退方案,画面表现和性能都会打折。
5.2 网格资产的Nanite转换流程
实际转换很简单:选中一个静态网格资产,在细节面板找到Nanite设置,启用Nanite支持即可。导入时也可以设置默认开启Nanite。引擎会自动把网格数据重新组织为Nanite资源。
但“启用成功”不代表“适合启用”。我见过很多案例,美术把所有高模都勾上Nanite,结果场景里堆了大量小碎片资产。Nanite对大规模高精度资产的效果非常好,但对那些不到几百个三角形的小道具来说,启用它反而会增加调度和内存元数据开销,纯属负优化。所以第一步永远是判断资产体量和实际用途。
启用后建议打开Nanite可视化模式,看几个指标:
- 三角形密度分布:确认近处和远处的密度是否符合预期。
- Overdraw:如果一片区域Overdraw极高,说明几何精度过高或资产过于密集。
- 簇数量和页流送情况:看模型实际占用的资源规模。
这些可视模式在项目设置里打开后,可以快速定位是哪个资产吃掉了预算。
5.3 内容创作中必须绕开的限制
Nanite不是万能钥匙,以下内容类型需要特别警惕:
| 内容类型 | 限制原因 | 实际建议 |
|---|---|---|
| 半透明材质 | Nanite光栅化流程主要面向不透明几何 | 使用原始网格或传统替代方案 |
| 骨骼网格动画 | 几何体顶点动画会破坏页和簇结构 | 骨骼网格继续走传统Skinned Mesh |
| 世界坐标位置偏移 | 旧版本完全不可用,新版本支持有限且开销大 | 需要谨慎测试,必要时关闭Nanite |
| 顶点色和Mask | 支持情况随版本变化 | 确认目标版本后再决策 |
| 布料/软体模拟 | 几何形变无法匹配DAG结构 | 维持传统Mesh,不要强行上Nanite |
这些限制的本质原因都一样:Nanite的优化建立在“静态几何”的前提下,一旦几何形状在运行时发生较大改变,它的所有剔除、页表、缓存机制都失效了。
5.4 实测中的调试手段与性能指标
遇到性能问题时,我通常按这个顺序排查:
- 确认Nanite是否真的启用了。用可视化模式查看资产颜色标记,很多“我开了Nanite但没生效”的问题其实是在Content里没启用。
- 观察流送状态。快速移动相机时,如果几何体频繁以粗糙形态出现,说明流送跟不上。可以尝试调整流送预算,但这只是缓解,根本方案是降低同屏资产密度或减少超大单资产。
- 检查材质占用。Nanite的材质解析是在可见像素上执行的,如果材质节点太复杂,即使三角形数很低,GPU依然会卡在采样和计算环节。
- 观察阴影更新。如果开启了Virtual Shadow Map,注意阴影页的更新频率,大片动态阴影区域会造成额外开销。
我也用过一些控制台命令去单独验证光栅化开销。记得在实际调优时先把Nanite、阴影、光追分别隔离,只看单一指标的变化,不要混在一起调,否则你永远找不出真正的瓶颈。
5.5 串流和遮挡剔除的细节
Nanite的流送和遮挡剔除配合是关键。持久化遮挡剔除会依赖上一帧的深度信息,如果场景中有大量高速运动的物体或频繁切换的相机视角,遮挡剔除的命中率会下降,你会发现GPU开销比静止场景高不少。
我在项目里常用一个笨办法:把相机路径固定,跑一遍场景记录帧时间曲线,再打开Nanite可视化看簇的可见性。如果某个镜头切过去后第一帧出现了很明显的粗糙几何,那多半是流送页还没到位。这种场景可以考虑预加载或延迟切换相机,比单纯提高流送预算更实用。
6. 我的选型经验:这些内容仍然不适合Nanite
讲了这么多Nanite的优势,最后说说我在实际项目中做出的取舍。Nanite很好,但不是一个“所有模型都该开”的功能。
6.1 动态形变、半透明与大型植被的取舍
在动态形变方面,骨骼网格、布料、植被摆动等场景里,Nanite优势发挥不出来。因为模型拓扑在变,任何缓存和剔除数据都会失效。我见过团队非要把树叶和草做成Nanite网格,结果每帧都在触发冗余计算,帧率反而下降。
半透明材质也是一样。Nanite设计目标是高性能不透明几何,半透明物体需要排序混合,和Visibility Buffer的流程冲突很大。与其在Nanite里硬撑,不如把这类资产保留为传统网格,场景中混用两种管线才是常见工程做法。
6.2 面向移动端与低端硬件的替代方案
移动端的GPU通常在缓存和带宽上远不如桌面GPU,Nanite的Compute光栅化路径在移动端能不能稳定发挥,取决于具体芯片。如果项目主要面向中低端移动设备,我会建议继续使用传统LOD和实例化方案,因为Nanite的调度开销在移动端不一定能换来足够的收益。
我自己目前的判断标准是:目标平台如果是高端PC、主机,且项目里有大量高精度静态资产,比如扫描资产、建筑可视化、数字孪生场景,Nanite是首选。如果是偏移动端、偏动态卡通风格、场景里以角色动画和半透明特效为主,我会把Nanite的使用范围压缩到几个关键静态资产上,甚至全程关闭。
回到开头提到的扫描车模型,它最后在生产场景里一直开着Nanite,配合Virtual Shadow Map效果很稳定。但这不意味着每个项目都应该复刻这个配置。我现在的体会是:Nanite让我们重新拥有了“把高模放进去”的自由,但自由同样要求你更清楚什么是“不需要的精度”。这才是它在渲染流程之外,给我最大的提醒。