今天由来自Eastshade工作室Danny Weinbaum分享在Unity中对森林植被进行优化的技巧。创建3D森林的时候不仅需要关注艺术技巧,更需要了解拥有什么资源以及如何进行放置。其中最重要一项需要考虑是:一个茂密森林的密度以及随之而来的性能优化。

074237mmkf8jnn6mxhami1.jpg

问题
解决森林优化的方法有很多,但是关于如何创建资源的通用指南却不多。不过关于森林优化有一样事情是共通的,即头号敌人都会是绘制调用(Draw Call)。

074238sefh3jgtisuzs3us.jpg

虽然多边形数量也很重要,但是问题并没有那么复杂。你只需要知道一个合理的目标值。

下面是一些我所制作的植物资源以及它们的多边形数量,可作为对应合理数字的参考。如果发现需要大量的Alpha Card才能获得所需的树冠丰茂程度,你可以在纹理中增加树叶的覆盖量。

074239zc5fhdzp11offm5d.jpg

还有过度绘制(Overdraw),当在Alpha Card和对象之间存在大量重叠时会发生过度绘制。我没有考虑或关注过它,我不打算修改树的轮廓以减少重叠。实际上,我不会为除了使它们看上去更美之外的任何原因去修改树木的轮廓。我也不打算修改森林的布局,以避免植物重叠。让森林真实而又不必操心过度绘制太难了。所以我把精力主要集中到了降低多边形数量和绘制调用上。

074240ktrlrer3eeurtdkv.jpg

计划
要解决这个问题,我们需要一个计划。大多数的计划要求在开始创建森林的时候,就考虑到优化策略。如果森林由来自不同来源的资源组成,可能很难做到这一点。减少绘制调用的策略有很多,在本文中我将分享我所使用的策略。

我的目标是将LOD1网格(第二个LOD阶段)组合为巨型网格,这样当玩家移动到远处时就可以整合绘制调用。我计划中的第一步是让森林里的所有东西,或者可能接近的东西都使用同一个材质。

森林中所使用到的资源都共享同一种材质。因此我需要所有的植被从同一个材质采样。

074241r3h21ybq613bb8nz.jpg

选择这样做的原因,是因为可以确保任何一簇的植被资源在合并时,都能成功整合成一个绘制调用。如果每棵树或草都使用唯一的材质,合并后的网格可能会拥有许多子网格,从而产生许多绘制调用。

我为需要的资源准备了一个2048x2048的区域,然后在创作的过程不停地将它们添加进去。这个过程是可以自动化,需要通过脚本修改所有植被资源的UV,不过有时候手工的方式效率更高。

分组
最终的目标是合并,但首先需要确定合并的方式。生成一个超级大的网格不是什么好主意,这有二个原因:

1、Unity中的网格索引缓冲区是16位的,这意味着每个网格最多只能有64k顶点。请注意:Unity2017.3及以上版本已经正式支持32位的索引缓冲,所以这已经不是问题。

2、无法对单独的分组使用LOD或从视锥体和遮挡剔除中获益,因为整个森林将是一整个网格。绘制调用将会很好很少,但是三角形数量可能多得吓人。我们还是可以用一些额外的绘制调用去拯救大堆的三角形的。

进入六边形网格。我编写了一个脚本,把所有的植物自动分到一个个六边形网格中。我选择了六边形而不是正方形,是因为六边形更接近圆,在进行玩家和每组之间的距离检测时可以更加精确。如果使用正方形,则会有一些角落离玩家太近。

074243asroaoooyzaoaspp.jpg

六边形分组随后会被组成一个个超级六边形。在一个特定超级六边形中的每个常规六边形都是处于LOD2状态,超级六边形会切换到一个合并版本的LOD2六边形,进一步整合绘制调用。

为什么不简单的使第一个六边形网格中拥有更大的六边形呢?因为更小的六边形可以在过渡到远的LOD时,能拥有更好的粒度控制,以及更精确的剔除。当物体足够远,多边形够低时,它们可以被合并成更大的六边形。

合并
在最初构建系统的时候,我合并了LOD0以及LOD1,但这样占用的内存太多。它会使高多边形版本植被资源的顶点都拥有唯一的内存占用,因为每一个合并的网格都是唯一的。此外网格越大,视锥体剔除效果就越差,最后导致更多额外的三角形被绘制。很可能会离LOD0六边形很近,或站在它们之上,因此视锥体剔除不精确的问题会更加突出。

我发现仅合并LOD1可以达到最佳平衡。LOD1多边形足够低,占用内存少,但节约的资源最多,所以我加载的世界其大部分将是处于LOD1状态的。

在Unity中进行网格合并没那么轻松。我必须要自己编写脚本,因为资源商店里的那些脚本无法处理子网格或顶点颜色。我选择不使用内置的LOD组件,因为它也仅是一个数据容器而已,六边形分组会处理LOD切换。我简单的使用了一个MonoBehaviour,并引用了处于未激活状态的子游戏对象,这样可以轻松的从渲染器获得材质与子网格了。

074243au1yo6n2d216nqya.jpg

使用六边形组进行LOD化和剔除
有件重要的事情要了解:Unity内置的LOD组件性能不佳。特别是当所有的草丛上都有这个组件的时候,在CPU上会进行有许多的距离检测。将世界划分为整洁的六边形组有很多好处,其中之一就是可以对每一个组进行距离检测,而非每一个子对象。然后将整个组的LOD都设成一致。我甚至还有一个用了着色器中的Alpha cutoff属性的动画过渡。

074252z64w533555xbqnl6.jpg

创作平滑LOD过渡
一个平滑的LOD0到LOD1的过渡通常要比一个基于LOD0的低多边形要好。如果过渡够漂亮,就可以把LOD距离移得更近些,从而减少屏幕上的总多边形数量。

我特别为树木做的一件事就是按照LOD1的样子进行创作。我使用3D包中的树枝实例来构建树。这使我可以为一些单独的树枝做一个LOD模型,然后通过将所有实例更新为更低多边形的版本,创造完整的LOD1。

074253l8h9g9hbbhzbfgkf.jpg

地形
地形有点超出了本文讨论范围,请注意:我没有使用Unity的内置地形系统,因此我的所有植物都是常规的游戏对象。

我选择使用常规网格而非地形系统,有三个原因:
1、默认的Unity地形系统性能不佳,它会产生数百个额外的绘制调用和上千个分布糟糕的多边形。使用常规网格可以按照需求来控制多边形分布。
2、为默认地形系统编写着色器很受限制。
3、我有许多的洞和悬垂物。我使用的着色器非常简单,它是一个带宏覆盖和法线的三通道Vertex Splat。

074255o44q6xl7o9g67l77.jpg

底纹
使植物着色器的一般性能,又称为像素填充率合理优化十分重要。

如果使用的是延迟渲染路径,那么将植物着色器完全延迟可以节省大量的渲染时间。我曾使用的是正向渲染,当我了解如何让相同的着色器使用延迟渲染之后,节省了30%的渲染时间。

创建具有半透明度的完全延迟着色器并不简单,因为需要访问通常在延迟着色器程序里无法访问的光线衰减。我使用了一个带自定义光照模型的表面着色器,它把一个非常低保真度半透明遮罩写入到G-Buffer中未使用的2位 (RT2的alpha)。然后我在Internal-DeferredShading.shader里添加了半透明函数。

光照烘焙
为森林中的所有植物烘焙光照将会产生很大的光照贴图内存使用量,在生产阶段难以接受的烘焙时间,而且最后结果看起来也并不是那么好,因为Alpha Card通常不会生成太好的烘焙效果。我将光照探头用于比建筑物小的任何东西。我为树使用光照探头代理体,这样树冠部分就能有一个到更浅颜色的漂亮渐变。

由于树不是静态对象,因此无法被光线映射器所见。我需要一种可以手动将探头包围区域变暗的方式。我编写了一个简单的脚本,将某个给定代理体内所有探头按所选的颜色进行染色。

074257vijjgz00ijkj80ev.jpg

技巧
对树的上半部分使用LOD1: 有些树足够的高,你可以让树冠部分保持低多边形。

枯树,或没有树冠的树干:想要达到想要的树冠密度,在没有树冠的情况下增加树干是一种可以使森林看起来更厚的节约做法。

巨量面片资源 :在地图中较平坦的部分,可以将大量草的面片合成单个对象,从而减少绘制调用,即使目标对象还处于LOD0/未合并状态。

总结
下面是对茂盛森林与植被进行的性能优化的总结。
绘制调用很可能会成为最大的问题。你需要有一个计划去减少它们。 多边形数是相对简单的问题,只要确保每个资源的三角形数量合理。 我忽略了对于过渡绘制的考虑,因为无法在不破坏美观的前提下做到它。 所有的植被纹理都图集化到了一个材质,确保合并网格能成为单个绘制调用。 我使用分组系统,将所有网格的LOD1都合并到一个组里。 我没有合并LOD0,因为它们的多边形太高,占用太多内存。 分组不能太大,否则无法从视锥体或遮挡剔除中获益。 我使用自定义的LOD化脚本,因为我是根据分组而非对象进行LOD切换。 我使用自定义的网格合并脚本,处理顶点颜色和子网格。 我手工制作的植被资源,经常在提前做好LOD1的计划。 可以将Alpha cutoff动画化,制作更平滑的过渡。 我使用常规网格,而非Unity的内置地形系统。 我编写了一个完全延迟的植物着色器,以保持低像素填充率。 我使用光照探头来为森林提供照明,并为森林中的包围区域使用自定义染色体。


以上内容就是在经过多次试错后形成的结果,我将它用于世界中的许多对象,并不仅仅是针对植被。当有许多同类对象时,例如:木桶或岩石,它也能工作得很好。最好的情况下,一个对象的多个实例会被整合到单个绘制调用,而最坏的情况下,也仅是跟原先保持相同数量的绘制调用而已,不过会消耗更多的内存。

更多Unity性能优化经验分享尽在Unity官方中文论坛(UnityChina.cn)! 优化, 性能锐亚教育

锐亚教育 锐亚科技 unity unity教程