GAMES104课程笔记22-GPU-Driven Geometry Pipeline-Nanite
这个系列是GAMES104-现代游戏引擎:从入门到实践(GAMES 104: Modern Game Engine-Theory and Practice)的同步课程笔记。本课程会介绍现代游戏引擎所涉及的系统架构、技术点以及引擎系统相关的知识。本节课主要介绍GPU驱动的几何管线与Nanite技术。
Introduction
Traditional Rendering Pipeline
Nanite是虚幻5引擎中提出的虚拟几何系统用来实现渲染超高精度的网格。要理解Nanite首先要回顾一下经典渲染管线:当我们从CPU端发出渲染指令时会首先由CPU来准备各种渲染所需的资源,然后GPU会接收这些数据并计算实际的着色。这种模式的缺陷在于CPU可能无法跟上GPU的计算速度,而且CPU的算力会浪费在准备渲染素材这一过程中。

随着渲染场景的复杂度逐渐提升,CPU端的计算开销会成为整个渲染过程的瓶颈。


Compute Shader
为了提升渲染效率人们开发出了compute shader这样的技术,其核心在于把过去只能在CPU端执行的通用计算转移的GPU端,从而节约掉大量的CPU到GPU端的通信开销。

Graphics API
在图形API层面上过去只能一次绘制一个网格,而现代图形API则支持在一次DrawCall中同时绘制多个网格。

GPU Driven Rendering Pipeline
总结一下,现代GPU驱动的渲染管线核心思想在于把CPU端的计算直接移动到GPU端,同时渲染所需的数据也会直接由GPU进行加载。在理想情况下CPU端只负责发出绘制指令,一切渲染数据加载和计算都在GPU端直接完成。

GPU Driven Pipeline in Assassins Creed
游戏工业对GPU驱动渲染管线的大规模应用可以追溯到《刺客信条:大革命》。在游戏中我们可以看到大量的拥有真实细节的建筑和场景,如何渲染这些极其复杂的几何对象是整个渲染管线的巨大挑战。

游戏开发团队提出了mesh cluster rendering的技术来提升渲染效率。mesh cluster rendering的思想在于对同一物体上的面片进行聚类,在渲染时首先根据cluster来判断面片的可见性

整个游戏的渲染管线如下图所示。通过clustering的方法可以去除掉大量不可见的对象以及三角形,从而极大地缓解了GPU的渲染压力。

而在CPU端只负责非常少量的视锥剔除等工作,初步过滤掉不可见的物体。

然后GPU端会把过滤后物体上的cluster拓展为chunk,每个instance可以属于不同的chunk而每个chunk可以包含不同的cluster。

GPU端进行实际的可见性剔除时会先检查chunk的可见性然后计算cluster的可见性。除了利用bounding box进行剔除外,还会同时结合三角形的朝向进行过滤,最后得到所有可见的三角形编号。

所有可见三角形的编号会存储在一个事先申请的巨大buffer中。写入过程是原子化的,因此可以利用GPU并行计算来高效处理。而在进行渲染时可以利用这个buffer来并行处理所有的三角形,从而实现对场景的渲染。


Occlusion Culling for Camera and Shadow
为了进一步提升渲染效率,除了剔除掉视野外的三角形外我们还希望能够把被遮挡住的三角形也同时剔除掉,这一过程称为occlusion culling。当相机在场景中的运动比较光滑时可以把前一帧的深度图投影到当前相机位置上,再结合hierarchy z-buffer就可以估计哪些cluster和三角形是可见的。

Two-Phase Occlusion Culling
更现代的occlusion culling方法是使用上一帧和这一帧的两个z-buffer来实现。首先利用前一帧的z-buffer来快速选取可能可见的物体,然后使用这些物体来渲染新的z-buffer。显然此时的深度图会有非常多的洞等待填充,而且很多像素的深度可能是错误的。为了修正这个问题还需要再利用这一帧的深度图来测试前面过滤掉的其它物体。

这种two-phase occlusion culling方法对于非常复杂的场景以及动态物体都有很好的性能。

而对于阴影的问题也可以复用前一帧阴影的深度图并结合hierarchy z-buffer来进行剔除。

要进一步提升阴影的渲染效率还可以结合相机的可见性,把所有相机方向不可见的物体全部剔除掉。


Visibility Buffer
和Nanite相关的另一个技术是课程前面提到过的G-buffer和延迟渲染,我们可以把场景中的各种几何信息记录在G-buffer中从而方便渲染时的计算。


显然这样的G-buffer会占用非常多的显存,这在画面高分辨率或是复杂场景的情况下读取数据的效率会变得极其低下。


V-buffer是为了提升数据读取效率和缓存利用率而提出的一种技术。V-buffer中不会记录太多的几何信息,一般只保存像素上物体的各种编号。


在进行着色时对每个像素需要先获取该处对应的三角形信息,然后通过插值来得到像素上相应的各种几何材质数据。这种渲染方式的优势在于计算量只与分辨率有关,而与场景的几何复杂度无关,因此拥有非常高的计算效率。


V-buffer可以很容易地和延迟渲染管线进行结合。我们只需要利用V-buffer中可见物体的编号来重新写入G-buffer就可以完美融入延迟渲染管线中。

当然V-buffer在实际使用时还有很多的细节要处理,比如说如何考虑纹理的梯度、如何选取合适的mip-map等。

使用V-buffer可以极大地提升具有复杂几何场景的渲染效率。

Virtual Geometry - Nanite
Overview
Nanite的核心任务是实现实时电影级高精度几何模型的渲染,我们希望能够尽可能还原有着无限细节的真实世界。


回忆基于virtual texture的技术我们可以为物体不同LoD的纹理烘焙在固定大小的纹理贴图上,在渲染时根据相机的位置和实际需要加载所需的纹理。这种材质表达可以提升缓存利用率以及数据加载效率。

Nanite的思想与virtual texture非常相似,不过Nanite更关心的是如何建立虚拟的几何表示。当然几何数据本身要比纹理贴图要复杂得多,如何建立规范的几何表示至今仍然是一个难题。


以体素化表示为例,尽管体素本身是相对规范的但由于其巨大的数据量我们很难在游戏引擎中来直接使用。

另一种流行的几何表示方法是曲面细分(surface subdivision),基于这样的技术我们可以把粗略的几何表面细分为高精度包含各种细节的曲面。然而曲面细分的一个缺陷在于很难对曲面进行降采样,即从高精度曲面来获得低精度表示。

其它的几何表达方式包括displacement map或是点云也都无法满足我们的需求。


因此在Nanite中还是选择了三角网格来表示,然后设计了一套非常复杂的算法流程来表达几何信息。

Geometry Representation
Nanite的一个重要想法是利用屏幕的精度来控制渲染时所需计算三角形的数量。尽管三角形的数量可以随着模型精度的提高不断增长,但只要屏幕分辨率不变所需绘制的三角形数量应该是比较稳定的。

因此可以结合前面介绍过的mesh cluster来控制模型的细节。

然后根据相机与模型的相对远近关系来生成cluster在不同LoD下的几何表示。


在选择cluster的LoD时需要考虑它投影到屏幕上产生的误差。一种直观的选取方法是当误差小于1px时选择当前层的LoD,否则选取下一层的LoD。



但是在合并cluster时需要考虑不同LoD的cluster之间可能会出现缝隙。当然我们可以把cluster的边锁住,这样不管是使用哪一层的LoD都会有一致的边界。不过这样的处理并不是一个非常好的办法,可能会产生严重的artifact。


Nanite中提出了cluster group的概念来处理cluster之间的缝隙。cluster group之间的边界会被锁住,而内部的cluster会在生成LoD时一起进行简化。

整个cluster简化的过程如下。需要注意的是简化后的cluster与原始cluster之间并不是一对多的关系,而是多对多的关系。即不同的简化后的cluster可以对应同一个原始cluster。





随着LoD的提高不同cluster group的边界也会发生相应的变化,这样可以避免出现高频噪声。




实际上这样的简化cluster过程可以表示为一张DAG,每个cluster在上一层LoD会有多个指向。



而网格本身的简化则可以使用经典的QEM等简化算法来实现。


Runtime LoD Selection
进行渲染时需要根据相机的位置来选择合适的LoD。不过对于DAG这样的数据结构进行访问时仍然是比较复杂的。


Nanite还使用了并行化的技术来加速访问。






除此之外还可以使用BVH来加速LoD选择。




BVH的构建过程还可以使用job system来进行加速。


Rasterization
Nanite在渲染时很多三角形的大小已经接近于屏幕上的一个像素,此时需要硬件光栅化来提供支持。



传统光栅化对于小三角形的支持不够好,在Nanite中会结合compute shader来实现软光栅。




在深度测试时,Nanite还利用了一些trick进行加速。实际渲染过程与V-buffer渲染过程类似。







Deferred Material
Nanite在绘制材质时会把材质信息转换为深度图,然后对可能出现的深度(材质)进行遍历。这样可以一次性绘制所有具有相同材质的像素。



更新的Nanite版本还会把屏幕划分为若干个tile,然后在每个tile上统计出现的材质。这样可以加速对全屏材质的遍历和绘制。





Virtual Shadow Map
高精度几何模型还会导致阴影渲染时的困难,而且遗憾的是Nanite目前尚不支持实时光追来计算阴影。


不过计算阴影时也可以结合LoD,在距离相机不同远近的位置使用不同精度的模型。



在这种思想下Nanite提出了virtual shadow map来表示不同精度的物体。


对于不同类型的光源也可以定制划分virtual shadow map的方式。

当相机和光源都不变时我们可以把shadow map相关的信息写入page中方便下一帧读取。而如果相机和光源发生变化则只需更新一部分page即可。


当然这种virtual shadow map在场景光源发生变化时会出现一些问题,因此比较适合主光源不变的场景。



Streaming and Compression




