教程|在Unity 5中优化SkinnedMeshRenderer

2016年03月02日 13:36 0 点赞 0 评论 更新于 2025-11-21 19:53

“过早优化是万恶之源” —— Donald Knuth

不少开发者在前期开发过程中不太关心算法等方面的开销,更倾向于采用简单的方式解决问题,必要时再进行优化。这种做法能显著加快开发进度,同时保证代码的简洁性。然而,在开发后期,图形资源往往会成为性能瓶颈,而优化图形渲染颇具挑战性。

本文将分享作者Lamebait在使用Unity 5制作沙盘游戏过程中优化SkinnedMeshRenderers的具体步骤,供大家参考。

第一次迭代:使用不带动画的网格

每个城市街区大约有15个网格,首要问题是大量Mesh不断被加载到游戏中。当街区数量增加到数百个时,DrawCall数量会飙升到大多数硬件难以承受的程度。虽然Unity支持网格批处理,能够减少一部分DrawCall,但由于所有网格都带有动画,使用的是SkinnedMeshRenderer组件而非MeshRenderer,而Unity不支持对SkinnedMeshRenderer进行批处理。

第二次迭代:SkinnedMeshRenderer.BakeMesh()

游戏中的所有建筑和树木仅在出现和消失时会有渐入渐出的动画,稳定后则完全静止。那么,能否在此期间将它们转换为MeshRenderer呢? 若使用带有MeshRenderer的无动画网格,并通过开关来选择使用MeshRenderer还是SkinnedMeshRenderer,可能会发现部分网格与其SkinnedMeshRenderer的最终状态不匹配,此方法不可行。

SkinnedMeshRenderer中有一个BakeMesh()函数,其功能是按照网格当前的动画状态创建网格数据快照,并输出该网格数据供其他地方使用。这样就可以将输出的新网格传递给MeshRenderer,从而享受自动批处理功能。但实际情况并非如此简单。

我们创建一个包含各种网格类型的字典,使多个MeshRenderer可以共享同一个网格,进而让Unity进行批处理。每当有新的网格加入时,会将其烘焙并添加到字典中。然而,这种方式会导致网格类型切换后,网格的高度略微拉伸。这是因为建筑的缩放值不为0,SkinnedMeshRenderer输出的网格本身已被缩放一次,而应用到GameObject上时,由于GameObject本身的缩放值,网格又被缩放了一次。解决这一问题的方法是先将建筑的缩放值设为1,烘焙后再恢复到原始值。

经过处理,整个切换过程看起来完全无缝。但这是有代价的,因为Unity不支持对使用了阴影的MeshRenderer进行批处理。而关闭阴影会严重影响游戏效果,因此需要寻找其他解决方案。

第三次迭代:网格合并

还可以进行手动批处理,因为网格可以合并成一个大网格。不过,这存在一些限制,例如网格材质必须相同,单个网格的三角形数量有限等。好在该游戏的建筑和树木总共只使用了三种不同的材质,因此可以将整个街区合并成最多三张大网格。

具体操作是,创建CombineInstance,将BakeMesh()输出的网格传递给它,并添加Transform。关键是要确保Transform的变换矩阵中GameObject本身的缩放值无效,否则网格会像前文所述那样发生错误变形。合并后的网格最终结果将应用到网格之前所在的GameObject上,并在街区状态变为静止后移除。

整个过程较为复杂,游戏中的许多因素都可能导致奇怪的效果,需要对每个因素及其可能的组合进行单独测试。但结果证明,这种方法是值得的,现在游戏在有阴影的情况下帧率也相当可观。

第四次迭代:更高一级

该游戏的一个设计特色是每个阵营最终的街区在生成后会被保留。如果街区从未改变,可以将它们合并在一起。但逐个合并会受到前面提到的网格大小限制。将城市划分为几个格子,可以将几个街区的网格合并为一个,从而进一步降低DrawCall。

总结

本文的三个关键要点如下:

  • 不要过早进行优化,可以适当推迟。
  • 善于发现问题!如果某个方法不适用于X,能否暂时将其转换为Y呢?
  • 运用创造性技巧来换取性能提升。

内容下载链接

https://github.com/Ryxali/StateCapital

来源

Unity官方平台

作者信息

洞悉

洞悉

共发布了 3994 篇文章