BroadLeaf:影视级别大规模森林的实时渲染技术

腾讯游戏 2023-06-14 13:43

以下选自演讲内容:

背景信息

首先让我提供一些背景信息,确定主要挑战和需要解决的问题。我还将讨论现有解决方案及其弱点。

实时树木渲染对于 3D 游戏、虚拟现实、地理环境可视化等许多应用非常重要。但是,由于缺乏实时渲染高质量大规模树木的解决方案,目前这些应用中渲染的场景要么只有低分辨率的树木,要么树木很少。我们希望找到一种解决方案,让艺术家可以自由设计大型森林场景,让用户在数字森林中有身临其境的体验,从而缩小虚拟世界与现实世界的视觉体验差距。

我们这里要解决的问题是实时渲染大规模树木,同时我们希望渲染出来的树木具有较高的视觉质量,最好是可交互的。这是一个重要但难以解决的问题。

那么问题为什么是困难的呢?主要原因有3个:

1.树木模型通常具有复杂的几何形状和纹理。

2.树木模型需要大量的 GPU 内存用于几何和纹理,特别是对于高精度的树模型。

3.树模型的流数据也可能很复杂。

现有的方法中,大多数能够保持树木模型的高视觉质量的方法都是基于细节层次 (LOD) 的。在今天的演讲中,我们只关注基于 LOD 的方法。

现有方法存在一些局限性:

1.LOD 生成不是全自动的,可能需要手动修改,这需要额外的工作

2.高复杂读的树模型的 LOD 切换对于实时应用来说效率不够高。即便速度足够快,由于简化的 LOD 网格和原始模型有差异,LOD过渡也可能不平滑,导致视觉上的跳变。

3.这些方法可能无法实时渲染包含数百万棵具有高视觉质量的树木场景。

这里我们给出一些具体的例子来说明现有方法的局限性。

这张幻灯片显示了一个包含细小叶片的树木模型,我们将其作为过度简化问题的示例。正如我们在视频中看到的那样,当我们的视点远离树时,树叶甚至细枝都消失了。

这是因为当从远处观察树时,该方法倾向于使用更简单的网格来表示树叶的几何。但是这种更简单的网格的生成可能会合并简化像叶子这样的小特征,从而使它们消失。

这里展示了渲染效率问题的另一个示例。在我们场景的这张截图中,渲染了约6亿个三角形。

对于这样一个复杂的场景,如果我们使用 Nanite Foliage技术来加速树木树叶的渲染过程,光栅化步骤仍然不够快,无法进行实时渲染。如图所示,光栅化总共耗时约14ms,是整个渲染时间27ms的一半。实际上,对于薄而细小的表面,比如草和树叶,很容易造成大量过度绘制(overdraw),导致效率降低。Nanite 会优化和删除网格中的三角形已达到简化网格的目的。但是对于一个又小又薄的表面网格,比如树叶,几乎没有三角形可以移除,所以很多重叠的三角形被同时渲染。

这些限制激发了对改进目前的解决方案的需求。在了解问题和现有解决方案后,我们希望能改变当前困境。我们的目标是:实时渲染由单叶可交互树模型组成的逼真大型森林——这在当今游戏开发中有很大需求。

我们还想为树叶开发一种自动的细节层次(LOD)策略,可以平滑高效地过渡并同时保持树叶形状。LOD有助于提高树叶的渲染效率,而在大多数情况下,树叶是植物模型中最复杂的部分。

注意到,我们的方法主要是为在高性能GPU上运行的应用而设计的,例如云游戏。

这是一个由数百万棵树和数万亿个三角面组成的场景。我们的目标是处理这种海量数据并实时渲染此类场景。

BroadLeaf方法

现在让我介绍一下我们的方法BroadLeaf。在这一部分中,我将讨论我们方法的算法和技术细节。

我们提出了一种新方法BroadLeaf,可以实时自动高效地渲染逼真的大型森林场景。这是通过降低植物叶子的几何复杂性和纹理规模以实现快速渲染来实现的。我们方法的流水线可以分为 4 个阶段:

1.我们首先设计了一个新的叶片索引结构来压缩输入模型的几何和纹理。

2.然后我们应用自动生成细节层次(LOD)和快速 LOD 过渡来减少渲染负载,同时保持流畅自然的视觉效果。

3.接着,我们将输入的纹理信息烘焙到不同级别的 LOD 网格。

4.最后,我们通过将不同细节级别的树叶组织储存成层次型数据结构,并使用基于GPU driven的设计,用Mesh Shader进行加速来进一步提高渲染效率。

基于我们的 LOD 数据的层次结构,我们可以很容易地添加更多的加速方法,如遮挡剔除,以提高渲染效率。我们还可以添加树木和场景中其他角色的互动。

我们的输入是一个带有纹理信息的三角形网格,用来表示植物模型的叶子。我们只考虑具有显式几何的树模型,这意味着这些模型由三角形网格表示。这是最通用的树模型表示,因为树建模工具(如 SpeedTree)通常会生成基于三角形网格的树模型,并且我们也可以轻松找到树木的网格模型数据集。请注意,我们将植物叶子与植物的其他部分分开处理。通常,树叶和其他部分(如树枝)是不相连的网格,但如果不是,则可以通过分析纹理或颜色信息来区分它们。另外,我们只考虑静态生长状态的树,这意味着我们的方法不支持树叶生长或枯萎的动画。

图中是我们场景中的一个高质量树模型的示例,它包含约 2千万个面,需要约 500MB 的存储空间。

对于大多数树模型,每个叶子都是一个独立的连通分量,对于大多数建模树来说都是如此。此外,整棵树的叶子可以通过 3D 仿射变换从几种叶子形变而来,并且可能添加一些卷曲效果。我们称这些叶子为基叶。

为了高度压缩的树叶,我们设计了一种新的数据格式。我们构建了一个叶片索引结构,让大量的叶子被索引到几种基叶。这避免了存储重复的叶子的几何和纹理,从而显着降低了内存使用量,尤其是对于高质量超精度的输入模型来说。

存储压缩叶子的文件格式如下:

对于每个基叶,我们首先有一个三角形表面网格文件来表示它的几何形状(顶点、面和UV坐标)

然后,我们使用一个文件来存储索引到基叶子的叶子的转换信息。每个索引叶子的变换信息由12个浮点数组成:3个数表示平移,1个数表示均匀缩放,1个数表示旋转角度,3个数表示旋转轴。

当然,我们还需要树叶的纹理贴图。

在某些情况下,我们可以从 SpeedTree 等树木建模软件中提取叶片索引信息。然而,叶片索引信息在大多数情况下是不可访问或丢失的,因此我们需要根据输入模型来构造它。

我们设计了一个简单但有效的策略:

1.我们首先按材料对树叶进行预分类。

2.然后,我们将具有相同的排序好的UV 坐标的叶子归类为同类,因为每个叶子都是一个独立的连通分量。请注意,此策略可能会错误地将两种不同的叶子归为一类,但这种情况很少见。

3.我们使用树叶的有向包围盒来计算它到基叶的变换。

4.对于叶子的卷曲效果,我们简化为每个基叶只有几种卷曲方式,也就是说同一基叶的叶子只能有有限种卷曲方式。

可选地,叶子的卷曲效果也可以通过对扁平的基叶的网格添加变形来产生。

对于像这样扁平的基叶,我们可以为每个顶点分配任意角度的沿Y轴两侧折叠的,围绕 X 轴卷曲的,或围绕 Y 轴扭曲的形变。

这些数据不需要预先缓存,可以在线实时生成。这样,我们可以获得更加生动的树叶。

之前显示的模型是一个可以构建叶片索引结构的非常典型的模型:它上面的叶子看起来彼此非常相似。根据我们的分类结果,这棵树上的叶子可以分为 32 类。该模型的几何数据大小可以压缩到 1%。

使用我们的新数据结构,我们只需要 约6.4MB 来存储叶子的几何形状,而原始数据需要约500MB。至于纹理的压缩比,完全取决于基叶数与所有叶数的比值。

这在很大程度上减轻了 GPU 内存使用的负担。根据我们的测试,我们的方法对树叶的压缩率通常在 2 个数量级左右,在某些情况下甚至可以更高。

准备好数据后,我们可以生成不同复杂程度的网格。我们引入了一种策略来自动高效地生成细节层次 (LOD)。由于每片叶子都是一个独立的连通分量,这里我们首先展示如何将一片基本叶子简化为四边形(四边形由 2 个三角面片组成)。该四边形近似叶子的形状,并且在输入纹理烘焙到它上面的时候可以很好地保留纹理。由于我们将用四边形来表示和近似叶子,所以我们简称这个四边形为四叶(quad-leaf)。

为了得到四叶,如图所示,我们首先计算基叶的有向包围盒。我们使用主成分分析(PCA)方法得到基叶顶点的3个主轴,然后将基叶网格的顶点投影到3个轴上得到顶点在轴上的分布长度。这样,我们就可以得到3个线段(黄色标记),由此可以组成基叶的有向包围盒。

然后,我们将基叶的三角形投影到包围盒的横截面上,并取用通过包围盒中心且投影三角形总面积最大的那个横截面四边形来表示基叶(这里我们将其标记为浅蓝色)。

我们使四叶的法线指向与基叶三角形的平均法线相同的一侧。请注意,基叶的简化可以很容易地并行化和加速,因为它们是相互独立的。

因此,给定树叶的网格,我们将基叶简化为四叶,然后对这些四叶应用储存在基叶数据中的变换以获得所有叶片的四叶。此过程输出 LOD的 0 级网格。为了生成下一级更粗糙的网格,我们使用相同的方式合并四叶以生成一个新的四边形来近似合并的叶子们。更具体地说,在这一步中,我们不是计算基叶网格的有向包围盒,而是计算正在合并的四叶网格的有向包围盒。我们根据树叶的位置和方向进行合并。

该图显示了一个例子,其中包含约 600 万个三角形面的叶子输入网格被简化为 0 级到 2 级 LOD 网格,分别包含大约 30万、一万5 和几百个面。请注意,在渲染阶段,树上可能有不同级别的叶子,根据相机位置以不同的细节层级动态交换。

合并步骤使我们能够关联来自不同简化级别的四叶。因此,我们设计了一个层次结构来存储和组织所有层次的四叶树,我们将其命名为 LOD 森林。LOD森林由相同深度的LOD树(如图所示)组成。LOD 森林的根节点指向最简化的 LOD 级别的四叶。LOD 森林的每个节点指向其子节点指向的四叶的合并的四叶。LOD 森林使我们能够有效地在不同级别的叶子之间切换。

下一步,我们为四叶的 LOD 网格生成新的 UV 贴图。对于如左图N个四叶(2N个三角形)组成的网格,我们可以将UV域平均划分为M个网格,使用这个公式可以很容易地得到网格的数量M。然后,我们将前 N 个 UV 网格(标记为绿色)分配给 N 个四边形叶子。然后每个 UV 网格将被细分为 2 个 UV 三角形。

请注意,这是生成 UV 贴图的最简单方法,我们还可以生成更复杂的 UV 贴图,例如,使得2D 和 3D 三角形的变换更加共形。

输入中叶子的纹理贴图包括颜色贴图、法线贴图、次表面散射贴图和位置贴图。

为了生成四叶的纹理贴图,我们在它们上面烘焙输入纹理。我们在 3D 中进行投影,然后将投影映射到 2D UV 域。对于一组输入全分辨率的叶子所指的四叶,我们将这些叶子的三角形投影到四叶树。注意投影三角形一定会在四叶区域内,我们可以得到四叶上投影顶点的重心坐标。然后我们可以使用重心坐标获得投影顶点的 UV 坐标。输入三角形的纹理被复制到四边形叶子的纹理贴图中。像素的深度根据位置图进行调整。这是我们在 LOD 级别 1 网格上生成的四边形叶子的颜色纹理贴图的示例。我们可以在纹理贴图的放大图中看到,每个四叶上都可以投影多个输入叶纹理。

在我们得到树叶的 LOD 之后,我们需要随着树叶相对于相机的相对位置的变化在细节层级之间进行转换。尤其是在视频游戏等实时场景交互中,需要在 LOD 级别之间平滑快速地过渡。然而,现有方法要么无法在过渡期间保留树模型的形状,导致视觉上的跳变,要么为了平滑过渡存储更多级别而导致渲染非常慢。不同的是,我们的方法能够获得视觉上的无缝过渡和计算快速 LOD 切换。

在这里,对于每个四叶,我们用如下数据表示它:它的3d中心,3d切向量,3d副法向量,中心的2d UV坐标,以及UV切线长度和UV副法向长度。

然后我们可以在线获取四边形的几何形状。这大大降低了渲染期间访问几何数据的带宽。请注意,某些属性是半精度类型,这是因为已知它们的值范围足够小,可以使用半精度来准确表示。

为了在 LOD 转换期间保持树模型的形状,我们独立地为四叶选择 LOD 级别,从最高的 LOD 级别(最简化的模型)开始。基于我们构建的LOD森林,我们首先计算屏幕四叶尺寸,即根节点的四叶在屏幕上的副法线和切线向量的长度之和,以决定是否遍历到它们的子节点,即决定是否使用更精细的三角形面片。如果四叶的屏幕尺寸小于预设阈值,我们将停止遍历到较低的 LOD 级别。如果叶节点的四叶节点的屏幕尺寸大于阈值,我们使用输入的全分辨率来表示这个叶子,即用一个变形的基叶来表示。

虽然计算量看起来很轻,因为每个四叶树只由两个三角形组成,剪枝 LOD 树减少了计算量,但在大型场景中计算量仍然很大,因此我们使用 Mesh Shader来加速计算。

我们首先从 LOD 森林的根节点逐层计算四叶的屏幕尺寸。每个级别的计算都在Mesh Shader中完成,这意味着执行一次平滑过渡,其中一个线程被分配用于计算一个四叶的屏幕尺寸。如图所示,在森林的根级执行mesh shader后,屏幕尺寸小于阈值的四叶(如绿色标记的节点)将被直接传递去光栅化,同时停止遍历它的子节点。对于其他节点(标记为黄色),我们需要检查它们的子节点的屏幕尺寸。我们将这些子节点的索引存储在 GPU缓存中,以便下一次执行Mesh Shader时检查它们的屏幕尺寸。请注意,叶索引在 LOD 森林中排序后储存,以保持内存访问连续,以便更快地检索信息。

我们的渲染管道是GPU 驱动的:我们将计算数据存储在 GPU 内存中。这里我们展示了 GPU 驱动的运行时数据结构:对于每棵 LOD 树,我们将其包围球存储为 GPU 场景中的根节点,根节点数据结构如左图所示:它包括树的类型、中心和包围球的半径。

对于每个四叶对应的LOD树节点,其名为 quadleafnode 的数据结构包含前面提到的定义四叶的数据和指向 LOD 树上其子节点的指针。

下面是一个示例,展示了我们如何执行一次Mesh Shader:

我们首先使用来自GPU场景的数据加载到GPU缓存中,以便Mesh Shader可以访问数据(这里我们会做遮挡剔除,我们将很快讨论)

然后我们使用Mesh Shader并使用它来计算四叶的屏幕尺寸。请注意,线程数量和四叶节点数量一样

在Meshshader中,我们计算四叶的屏幕尺寸:当此尺寸大于预设阈值时,将直接绘制四边形。当这个尺寸小于阈值时,我们将这个四叶节点的子节点写进Mesh Shader缓存用于下一次计算。

注意这里我们不用computer shader而用mesh shader的原因是:

1.有了mesh shader,我们可以遍历树的细节,画出合适大小的四叶

2.Computer shader计算后不能直接渲染,我们必须写一个缓存然后读取缓存进行渲染,需要更多的GPU访问带宽。

这里我们用一个单树的例子来展示我们的LOD 过渡的结果:从视频中我们可以看到,在相机移动过程中树叶的过渡非常平滑。即使视点远离树,树叶的形状也保存完好。

右边的图1到图4显示了视点由近到远的过渡过程中树叶LOD级别的变化。我们注意到,一棵树在特定时刻通常具有不同 LOD 级别的叶子。例如,在图 1 中,树上有原始分辨率的叶子。对于距离相机较远位置的叶子,它们由 LOD 级别 0 和级别 1 的四叶表示。随着视点向后移动,在图 2 中,叶子是从级别 0 到级别 1 的四叶。在图 3 中,大部分四叶逐渐变为 1 级,一小部分四叶变为 2 级。在图 4 中,所有的四叶都是来自级别 1 和级别 2 的,主要是级别 2。

好的,到这里我们已经介绍完了我们方法的主要部分。在这里让我总结一下我们对整个算法的设计思路:

我们首先根据树的建模方式构建一个树叶索引结构来压缩输入的树叶。

然后我们为树叶生成LOD 并构建层次结构来组织 LOD 数据,我们可以在其中访问和控制叶级组件。

最后,我们可以应用GPU 驱动的渲染架构来进行计算和渲染:我们使用Mesh Shader来加速 GPU 中 LOD 过渡的计算,计算结果可以直接渲染避免读写缓存。

通过我们设计的这个流程,我们能够实时处理大规模数据,同时还能获得流畅逼真的视觉效果。接下来,我将讨论我们方法中的其他的一些技术细节。

首先是关于剔除。正如我之前提到的,根据构建的 LOD 森林,我们能够有效地剔除不可见的和被遮挡的树叶以优化渲染性能。剔除是在 GPU 上完成的。

我们首先进行视锥体剔除,排除边界框完全在视锥体之外的叶子,然后我们进行遮挡剔除。

剔除是根据我们的层级LOD 树逐级进行的,从 LOD 森林的根节点开始:如果某个节点完全看不见,那么它的子节点肯定在外面,不需要被检查。否则,我们继续检查它的子节点。

请注意,遮挡剔除通常很难在以前的树叶渲染方法中表现良好。这里有2个原因:

1.带透明度测试的深度预渲染使时间加倍。

2.像树叶这样的结构很难被很好地遮挡,因为树叶之间的间隙通常很小、分散且不规则。

使用我们的数据结构,我们可以在剔除时访问四叶级别的数据,这使得剔除可以在细粒度上进行,从而更有效。

此处显示的画面包含 约 170 万面片和约 1.94 亿像素,我们的剔除过程可以将渲染面片和像素分别减少到约 40万 和约 5200 万。数据大小被压缩到原来的 25% 左右。这在很大程度上缓解了在充满小结构(如树叶和草)的场景中经常发生的过度渲染的问题。

对于这个全景图帧,我们的剔除策略可以将渲染像素减少到原始数据大小的50%左右。

有时输入的树模型可能具有低分辨率和粗糙的叶子网格,这使得在近距离观察时叶子呈锯齿状不平滑。我们的目标不仅是渲染一个真实的全景森林,而要在特写视图中也能显示生动的树叶。

所以在这个场景中,当我们遍历LOD树到叶子节点、需要在原始全分辨率下渲染叶子时,我们不根据输入的网格来绘制叶子,而是根据对应于叶节点的四叶进行细分来绘制叶子。细分的深度取决于屏幕上叶子的面积。这里我们有个示意图,图中绿色的叶子是扁平的基叶,黑色的四边形是基叶对应的四叶。叶子占据的像素越多,四叶的细分应该越深。细分完成后我们会添加叶子的卷曲效果。

我们的方法还支持树叶与其他对象的交互。在这里,我们简要介绍一下在我们的树上应用交互的过程。首先,我们用自动蒙皮分别为树叶和树枝生成骨架。然后,我们使用硬件光线追踪来计算树和角色的碰撞。这里我们为树建立一个层次包围体结构,并在碰撞模拟过程中不断更新它。碰撞检测的光线来自角色模型的每个顶点,我们使用顶点法线作为光线方向。请注意,我们的树模型是单叶可交互的。

这里的树有大约 2万多个骨架和大约 50万个面片。互动的恐龙模型有大约 10 万个面片。动画可以 35fps 的速度呈现。

BroadLeaf与Nanite的对比

现在我们了解了我们的方法 BroadLeaf 的整体算法。我们知道为什么它能够实时渲染由单叶可交互树模型组成的逼真大型森林。这在当今的游戏开发中非常有用。此外,我们的方法是自动的,与现有方法相比无需复杂的参数调整。

我们将在接下来的部分展示我们的方法的渲染效率,同时将我们的渲染结果与虚幻引擎5 中最先进的大规模几何场景渲染技术 Nanite 进行比较。

在这里,我们比较了我们的方法与Nanite Foliage在渲染树叶方面的效率。我们使用 UE 5 为 Nanite 和我们的方法渲染场景。

对于这个场景的当前帧来看,nanite 渲染树叶需要大约 44 毫秒,而我们的方法只需要 3~4 毫秒:

类似的,在这帧中,Nanite 需要约 37 毫秒:

同样,当视点拉近时,nanite 需要大约15ms,而我们的方法始终需要 3~4ms:

该图显示了我们的方法与 Nanite 渲染测试场景的渲染结果对比。该测试场景由 4 种树木模型的 500 个副本组成,总共 2000 棵树。我们将这 4 块树的副本从近到远排列放置,如图所示。请注意,这里我们没有显示树枝,而只渲染树的叶子。

正如我们在左侧看到的那样,当从远处看场景时,Nanite 无法正确渲染树叶。远处的树叶都消失了。相反,我们的方法渲染的树的所有叶子都保存完好,如右图所示。如前所述,Nanite 的缺叶问题是由于它在处理细薄表面(如植物叶子)方面的局限性。对于此场景,Nanite 以 76 fps 的速度渲染场景,并且仅考虑树叶时,Nanite 使用 1146 MB GPU 内存和每帧 5.7 毫秒。我们的方法以 102 fps 的速度渲染整个场景,并使用 630 MB GPU 内存和每帧 2.5 ms 渲染树叶。这表明我们的方法具有更高的效率并且使用更少的计算资源。

我们注意到 Nanite Foliage 中有一个选项可以保持叶子的区域,该功能会在模型在视图中缩小时会放大每片叶子。这样可以一定程度上保存叶片,但仍然效果欠缺。

这是一个比较:这是一个包含 9 种不同种类的树并排放置的场景。我们在场景中放置了 5 排树,并将相机从近到远移动。这两个视频将在这个场景中展示对比结果,上面是Nanite Foliage(区域保持选项开启)的结果,下面是我们的方法BroadLeaf的结果。正如我们在红框中的区域中看到的那样,我们的方法可以更好地保留树木的形状,特别是对于原本叶子稀疏的树木模型来说。

BroadLeaf的潜在应用、局限性和未来工作

最后,我想展示我们demo场景的视频。我还将讨论 BroadLeaf 的潜在应用、局限性和未来工作。

由于植物叶片数据量庞大,大规模森林的逼真实时渲染一直是一个重要但难以解决的问题。为了解决这个问题,我们提出了一种名为 BroadLeaf 的新方法,用于实时渲染具有影视质感的大规模树叶。我们渲染的叶片具有显示的几何表达,因此可以单叶交互。如视频所示,我们的方法实时渲染了约 12 万棵树的约 2.3 万亿个三角形面的场景,这比现有的游戏引擎快得多。

BroadLeaf 的应用不仅限于游戏。这里让我们探索它的潜在应用,以发掘它可以提供的全部的可能性和好处。

1.BroadLeaf 可用于在虚拟现实或增强现实应用中以创建交互式的逼真的森林场景,为参与者提供引人入胜的沉浸式体验。

2.我们的方法还可以帮助可视化现实中的丛林景观。这可用于规划城市环境,或用于环境研究来可视化环境随时间的变化,或帮助创建热门旅游目的地的虚拟旅游场景。

3.此外,BroadLeaf 还可用于电影制作的虚拟制片——创建一系列虚拟的大型森林场景,提供演员与数字元素之间的互动。它允许电影制作人实时查看和调整,从而在后期制作过程中节省时间并降低成本。

当然,我们的方法中有一些地方可以改进或值得更深入地研究。最直接的一个是LOD网格生成的改进,特别是对于更高LOD级别的低精度的多边形网格。我们希望在保持平滑的 LOD 过渡的同时使用更少的面片,以便进一步提高渲染速度。

目前BroadLeaf 是基于光栅化的,它可以很好地处理可以构建叶片索引结构的植物。但它可能无法在难以构建叶片索引结构的植物上有效地工作,例如通常由隐式表面表达的草,或叶片大而起伏的香蕉树。

因此,一个有趣的方向是考虑使用光线追踪来解决这些植物叶片渲染问题,并以类似BroadLeaf的效率提供更好的视觉效果。这可能有几个优点:首先,基于光线追踪的方法不需要生成 LOD,而且我们不需要担心阴影贴图的分辨率或过度。此外,随着场景的复杂性加倍,光线追踪的成本不会像基于光栅化的方法那样加倍。