[Translate] Foliage Optimization in Unity | [译] Unity中植被渲染优化
原文在此,下为译文
在我上篇博客——创建森林的美术建议中,我指出了创建3D森林的一些要点。那里我主要强调的是美术建议,资源应该有什么、如何布置。森林一个重要的事情是密度,随之而来的问题是性能优化。

问题

有很多方式优化树林,但如何来创建资源,通用的不多。然而,有一件事是通用的,那就是关注drawcall。
虽然面数也很重要,但不那么复杂。只需要设定一个合理的目标。下面是我们的资源的一些面数。如果你需要很多的透明插片来达到想要的效果,你需要提高贴图中叶子的占用率。
最后,是OverDraw。如果你有很多重叠的透明物体。我不太会关注这个,因为其实我也不知道怎么办。我会修改树的轮廓减少重叠。但是美术效果更加重要。但让树林真实还得考虑overdraw太难了。我只关注了面数和drawcall。

计划

如果你希望发现unity的一些隐藏参数,可以来优化,那么抱歉我没写这些。你需要一个计划来解决这个问题。很多需要在创建资源时记着优化策略。如果你的资源时来自不同的地方,那么就很难啊办了。有很多种策略减少drawcall,下面会展示我的。我的目标,简而言之,就是把LOD1的模型合并成大mesh,这样远离玩家的drawcall就会减少。

图集

第一步是让森林中所有距离近的植物用同一个材质。这样最终我的所有植被只需要采样一张贴图。
这样的原因是成组的植被就可以用一个drawcall了。如果树和草用独特的材质,那么就算合并了mesh还是很多drawcall。把所有资源合到一个贴图上不是很难,因为一开始就是这样设计的。我设计了一个2k的贴图,并为所有的资源分配了面积。
这是可以自动化的,用脚本改变uv就可以了

分组

最终的目标是合并,但首先我们需要知道如何合并。全部合并成巨大的mesh是不明智的。首先:unity用的16位的index buffer,所以面数最高64k。其次,那样就没法LOD每一组了,因为整个森林是一个mesh。尽管drawcall很低,但面数很高。我们负担得起那增加的几个drawcall。
用六边形组。基本上我写了个脚本,自动把植物分成六边形的组。不用四边形因为六边形更圆,这样分组距离检查更为准确。方块的角落离玩家更近。
后面你会发现分成格子有更多的好处。
跟上我了?还有一层让我很痛苦,六边形组分成了大六边形,用于LOD2.这些超六边形是一个合并版的LOD2组,更加减少了DC。如果你想知道为啥我不让第一个六边形组大一点,是因为我想要更惊喜的LOD之前的切换。当东西更远了,模型可以合并成更大的格子。这基本注意不到因为小的六边形都在最远的LOD了。

合并

最开始的时候我合并了LOD1,但我发现这太占内存了。这需要高模的每一个资源有独特的内存空间。并且,模型越大,裁剪的粒度就越大,也就会渲染越多的模型。玩家倾向与离LOD0更近,因此不精确的裁剪的问题就夸张了。我发现合并LOD1是一个完美的平衡。它面数比较低,而且非常节省因为大部分的东西都在LOD1状态。
不行的是unity里合并模型不直观,我需要写自己的脚本。因为assetstore上的资源不能处理submesh和顶点色。至于如何组织LOD,这取决于你。我倾向于不适用原生的LOD组件,因为那只是一个数据容器而已,我的六边形分组来处理LOD切换。简单地说我用了个monobehabior脚本,带着GO的应用来激活GO。这样我可以轻易获取他们的材质和submesh

用六边形组裁剪和LOD

需要记得一件事:原生的LOD组件很费,尤其是有很多的草的时候。距离检查发生在cpu上。分成六边形组的好处是你可以用分组来检查LOD,而不是一个个的子物体。这样整个LOD组是一个单元,我甚至加了alpha test的过渡渐变。
另外动态的遮挡裁剪也用了分组。我没用原生的遮挡裁剪,因为它好像和多场景结合起来不太好。分组很少,所以可以完全动态用raycast检查是否在视线中。我每帧都做raycast。只是保证越过汕头和墙角时它们及时出现。你不可能对每一个物体都这么做,那太费了。但raycast一组20-50个物体时值得的,尤其是每帧都检查。我只用terrain来做遮挡体,因为森林里的其它东西不是可靠的遮挡体。

平滑的LOD切换

LOD0和1中平滑的过渡是很重要的,我做的是LOD0用一些instance的树枝,然后直接用低模的树枝instance替换作为LOD1.

地形

地形本来不是本文的重点,但我要指出的是不要用unity原生的terrain系统。我的所有植物都是gameobject。这有三个原因:1. 原生terrain性能很差,负担不起那么多的drawcall。 用普通的mesh可以控制哪里需要更多的面数。其次,原生terrain的shader系统限制很多,并且文档很少。最后,我有很多山洞和悬空物。我的地形shader很简单:三个顶点色通道控制混合,还有一个整体的叠加与法线。

着色

植物的shader需要优化好,完全延迟渲染会比较省,但我曾经用的前向渲染,但当我明白如何用延迟渲染时,省了30%的时间。创建一个完全延迟的植物shader,还带有透明度,不是那么直白。这需要采样光的衰减,而在延迟中不是正常拿到的。我意识到下面的部分会深入unity的细节。但满足你的好奇心,我用了一个surface shader,自定义光照模型,半透明蒙版放进GBuffer没用的两位里,然后在Internal-DeferedShading.shader里加了一个处理半透明的函数。我花了快一年了才搞明白怎么做。这篇文章帮助我理解了。我最终花了两年才明白怎么弄,我很愿意帮助没搞懂的人明白这是怎么回事。

光线烘焙

全部烘焙很占存储空间,而且烘焙时间很长。并且还不一定好看。因为透明的物体不总是有很好的烘焙效果。我为小东西用了用了light probe,为树用了Light Probe Proxy Volume,这样就会有到树冠上的颜色过渡。因为树都布置静止的,lightmapper看不到它们,所以一个方法来让lightprobe看上去暗一些。我写了个脚本给probe染了一个颜色。

杂项技巧

LOD1一半的树。很多树很高,上面树冠用低模就行了。因此我把LOD的贴图弄到一个上了。
让森林变得更密?可以只加一些树干
大片平地上可以让草整合成一个物体,减少drawcall。

GPU Instancing的笔记

unity5.4引入了instancing,用脚本来渲染才能用到它。这和结合mesh不太一样,这更好因为很多mesh可以直接整合到一个drawcall里,也不用担心整合mesh带来的内存负担。但也有些缺点。因为是用脚本渲染,需要维护那些模型是可见的。这你都得自己写,记住C#可能会比较费,而且把很大的东西传到gpu也会很费。
我试过GPU Instancing,甚至把我的分组系统替换成gpu instancing版本,但没有我的合并版本性能好。并且还有很多限制,比如lightprobe
还有一个新方法,DrawMeshInstanceIndirect,可以用compute buffer生成实例表,性能更好。但没有文档,我不是个好的程序员,我没试出来。

结论

  1. drawcall是一个大问题,要有计划地减少
  2. 面数比较简单,只要保证资源的面数合理就行
  3. 我忽略了overdraw,因为要保证美术效果
  4. 所有的植物贴图合成了一个材质,保证合并的mesh用一个drawcall
  5. 用分组系统,合并了LOD1的mesh
  6. 没有合并LOD0因为内存太大
  7. 分组不能太大,要不然遮挡剔除会失效
  8. 我用了自己的LOD脚本,因为分组做LOD而不是用单个物体
  9. 我用了自己的合并mesh脚本,因为要处理submesh和顶点色
  10. 我自己做的植物资源,做的时候记住LOD1的处理
  11. 可以用Alpha Cutoff做平滑切换
  12. 我用了普通mesh而不是unity的地形系统
  13. 我写了延迟渲染的植物shader,保证像素填充率比较低
  14. 为植物烘焙灯光不太可行,我只用了lightprobe

译者后注:

  1. Atlas是必选方案,不过如果场景变化比较多,如何规划和严格执行分区是个问题
  2. 合并mesh是必选方案,尤其是要分块加载的话,不可能动态static batching,必须预先合并和分组
  3. unity的terrain面数可以控制,但是drawcall太高了,不能使用,如何转mesh并保证低面数和高精度是个问题
  4. 树叶烘焙的话,表面积太大太耗时,译者也选择了放弃
  5. DrawMeshInstanceIndirect要用compute shader,效率比DrawMeshInstance好多了。不过似乎ios上用metal才行。高通骁龙不确定,反正麒麟好像阉割了一个buffer,不能用。DrawMeshInstance限定一个batch1023,还有CBuffer大小限制。

发表评论

邮箱地址不会被公开。 必填项已用*标注