本篇简单介绍下UE5的虚拟几何技术Nanite。UE5的Nanite是一款革命性虚拟几何体渲染系统,依托GPU驱动渲染技术与全新网格格式,打破传统多边形预算限制,实现像素级细节与海量模型的高效渲染,核心价值在于将几何渲染从“多边形数量约束”转向“像素细节驱动”。其通过分层聚类存储网格,导入时拆解为三角形组层级簇结构,支持连续LOD无离散跳变,同时高度压缩数据,仅按需流式加载可见细节,大幅降低显存开销。渲染阶段由GPU实时完成可见性剔除、LOD动态切换与光栅化,有效规避CPU瓶颈。

网络上关于Nanite的介绍文章有很多,有些讲的非常的详细,甚至从源码级别介绍Nanite的相关优化技术,这里就主要对比下Nanite和之前文章介绍到的GPU Driven技术的一些区别和联系。

Nanite对于网格的处理和之前的方法类似,都是将原始的网格转换为一个个的cluster来表示,后续所有的culling,rendering都是基于cluster级别来进行。但是Nanite和之前的游戏所面临或者说面向的场景的三角形数量的量级是不一样的,Nanite场景中三角形能达到几亿,甚至几十亿,当三角形的数量达到这个规模之后,对于cluster的处理会带来非常大的挑战。

首先是cluster的数量。一个cluster的三角形一般在64/128/256左右,这个数字是根据当前硬件平台的能力所计算得到的,所以数十亿的三角形划分得到的cluster数量能够达到几百万甚至上千万,在之前的算法中,GPU需要并行的处理每一个cluster,然后进行compact,最后发起drawcall。虽然现代GPU硬件能够支持大量的并发thread,但是如果并行处理几百万的cluster,这个耗时是不可接受的。

其次是模型的LOD。由于原始模型本身的面数很高,如果制作LOD,那么LOD的层级数量也会比较多,因为如果LOD层级太少,那么相邻的LOD变化会非常大,切换就会影响视觉效果。而一旦LOD的数量增加,那么cluster的数量也会继续增加,会对culling pipeline产生更大的压力。

Nanite解决这个问题的方法是使用Hierarchy Cluster LOD。如上图所示,Hierarchy的思想是每一级别的LOD都是从上一个层级的LOD经过简化再分割得到的,每个层级之间的LOD形成一个树形结构。

具体的中间流程如上图所示,对于层级N的cluster,首先进行聚类形成clustergroup,然后将这个group内的cluster进行merge,然后再进行simplify,得到原始面片一半的三角形,然后将这些triangle再进行划分,得到层级N+1的clusters,这样便完成了cluster数量的减半。上述过程一直进行到只剩下一个cluster为止。为了不产生LOD的crack,这里cluster group在做simplify的时候是lock edge的。

上图直观的显示了跨层级的cluster划分的效果。

所以实际划分的结果是一个DAG,而不是一个tree,这是因为层级N+1的group里的cluster是层级N的groups里的所有cluster的父节点,子节点对应多个父节点,而不是tree中子节点只对应一个父节点。

runtime的时候,根据视角对整个DAG进行cut,即可得到当前视角下可见的clusters。这里回到了之前的问题,如此多的cluster,而且一个DAG的结构,该如何进行culling呢?

首先在建立cluster LOD的时候,就保证了相邻的LOD之间的cluster group需要使用相同的boundary,确保切换的时候没有缝隙。第二个是经过group之后再划分的clusters,需要共用相同的culling data,包括boundinginfo和error,这个保证了每个group的cluster要么全部cull,要么全部可见,不会产生重叠。

有了上述的保证,那么实际上对DAG的culling就非常简单,对于每个cluster,如果parent error不满足,自己的error满足,那么就可见(被选择)。反之,如果parent的error满足,那就直接使用parent就行,自己就不可见,如果parent的error不满足,自己的error也不满足,那么也不可见。所以可以看到,每个cluster只需要记录自身和parent的culling data,就可以决定自己是否可见,那么整个LOD selection就可以并行了。

当然,如此多数量的cluster并行处理性能消耗还是非常大的,所以Nanite的处理方式是使用原来的树状层级结构,对LOD cluster group建立BVH结构进行加速处理。在最开始的Nanite版本中,介绍的是使用Persistent thread来进行culling,所谓的persistent thread就是开辟一个类似thread pool,然后使用生产-消费的模式,从BVH的顶层开始,逐层级的进行culling,每一个层级的节点如果当前不可见,那么就将其子节点输出到队列中,等待下一轮处理,直到所有的任务都处理完毕。在最新的UE5.7中,Nanite已经默认不使用persistent thread,而是采用每个层级dispatch一次进行culling thread进行处理,具体这么做的原因还不清楚。

Nanite的rendering的整个流程,包括VisibilityBuffer, Deferred Materials,Software Rasterization等相关技术在本篇文章就不详细介绍。Nanite中的工程优化思路与细节有很多,值得自己的学习和探索,后续打算专门出一系列的文章,结合Nanite的源码进行详细的解读。

参考文献