UWA

每天一点UWA:第十五周

Posted by 蔡华的博客 on November 13, 2017

AssetBundle

我们想请教一个Unity的普适性的资源管理问题。举个例子,我们现在的一个特效Prefab包含的贴图打成AssetBundle时没有单独拆分出来,就会存在一个问题,这个特效AssetBundle会存在重复加载的问题进而导致重复的特效贴图在内存中。(针对“重复加载”说明下:我们首先通过AssetBundle出来一个Object,这个Object会缓存一段时间,在这段缓存时间过后这个Object会被Destroy掉,而需要释放的特效是通过Instantiate这个Object出来的。当这个Object过了缓存时间被destroy掉后,下次需要释放相同的特效还是通过load同样的AssetBundle进行再实例化出来,这样内存中存在两份贴图了。)

  • 一般是团队中自行做一些资源的引用计数来进行管理。频繁调用UnloadUnusedAssets是不可取的(该函数的主要作用是查找并卸载不再使用的资源。游戏场景越复杂、资源越多,该函数的开销越大,一般在300~2000 ms范围内),但可以调用UnloadAsset来释放资源。
  • PS:典型的释放prefab后没有释放对于的资源。确实应该用UnloadAsset,或者一开始这个图片资源就应该做成单独的AssetBundle。

美术做粒子的时候,粒子与粒子之间共用资源的情况很多,例如某几个粒子共用一个Material,某几个粒子共用一个贴图等,应该如何组织AssetBundle?要是对应到最细的那个程度,凡是共用过的资源都单独打一个AssetBundle,好像又会很琐碎,假如不那么做,粒子与粒子之间的AssetBundle又会有冗余。这方面有什么好的建议?

  • AssetBundle打包没有标准的方式,单就粒子系统而言,因为其个体本身比较小,并且在项目中经常大量出现,所以并不建议将粒子系统逐个打包,而是建议根据其出现频率进行打包,比如将同一段时间、同一出现场景等的粒子系统打包在一起。同时,由于粒子系统的Shader基本上都是Unity内置Shader,因此,尽可能将Shader进行依赖打包。

Gameplay

我把骨骼文件的Optimize Game Objects”开启了,然后骨骼信息就没有了,那Avatar换装时候需要处理的骨骼信息怎么办?

  • 在开启“Optimize GameObject”选项后,因为Avatar信息消失,所以并不能通过原始的合并骨骼、合并Mesh的方法再来实现换装功能。对于开启“Optimize GameObject”选项的模型,Unity本身有另外一套更为方便的换装方式,即只要所换装模型的骨骼结点信息与Avatar自身骨骼信息可以匹配,那么直接将换装模型挂在Avatar模型下做为子节点即可,而不必再通过骨骼合并的方式来进行换装。
  • PS:mark一下,没看懂。

UI

关于Mipmap的设置

  • 通过修改Texture的mipMapbias这个值可以改变使用的mipmap的层级。但是这个值目没有办法在全局设置,在导入图片的时候要用代码设置。
  • unit官方建议Note that using large negative bias can reduce performance, so it’s not recommended to use more than -0.5 negative bias.,但是Mipmap的层级数的确是一个整数。mipMapbias是由Unity引擎定义的一个参数,按照Unity官网解释来看整数代表了比当前层级更低(级数更大,更模糊)的Mipmap,负数代表了比当前层级更高的Mipmap。“-0.5”具体是偏移多少层级目前也不是很清楚。
  • Trilinear应该是会比Bilinear效果好一些,但是也差强人意。原因是这样:传统的Mipmap(不采用Anisotropic Filtering)都是每层将u,v两个方向缩减一半,即:512x512的下一层是256x256。这就导致在两层交界处在不同层采样出来的纹理在u,v两个方向都被拉伸(或者叫变模糊)。Anisotropic Filtering的方式可以简单理解为在交界处只在某一个方向上被拉伸,另一个方向保持原有采样率(或者叫纹理分辨率),这样才能明显降低突变的模糊感。
  • 使用Anisotropic Filtering不仅纹理内存占用会增高,而且采样率也增高(因为有一个方向的保持不变),因此它比传统的mipmap更耗时,耗时在于GPU端对纹理进行采样时增加了访存,在CPU端没有影响。虽然Anisotropic Filtering 耗时增加,但是相比于直接增加mipmap level(也就是设置-0.5的偏移),要达到相同的效果,Anisotropic Filtering的时间耗时还是相对更低的。

我在改血条,我原来是一个Canvas里放了所有血条,后来改成每个血条一个Canvas,再改成每个血条完全不用UGUI,直接用SpriteRenderer绘制,感觉性能越来越差了,怎么办呢?

  • 对于“所有血条放一个Canvas”和“一个血条放一个Canvas”做一个比较:
    • 前者的开销主要在于网格的更新,在Unity5.2之后主要是在子线程中通过Timeline来查看。因此只看主线程的话,这种方法肯定是更高效的;
    • 后者的开销主要在于DrawCall的数量(前者理论上能做到只用1个DrawCall,后者一个Canvas即一个DrawCall),开销被包含在了Camera.Render或者Canvas.RenderOverlays中。
  • 因此,在选择时,需要考虑的就是“网格更新”和“DrawCall”的权衡。
  • 还是建议尽可能降低血条的顶点数,然后选择前者。

模型&动画

优化动画精度时发现针对Generic效果明显,而Humanoid变化不大。

  • 精度优化降内存(并非通过减少位数降低文本体积降内存),其实质是将曲线上过于接近0的数值(例如有效数字出现在小数点6位以后)直接归零,使部分曲线变为constant曲线来降低内存消耗。
  • 在Generic中,大量曲线存在这样的数值,因而降低精度后,constant曲线增加,内存降低。
  • 但在Humanoid中,动画信息被转化到Muscle空间后,muscle曲线上的数值很少约等于零,很难因为精度降低变为constant曲线,因此内存占用受精度降低的影响不大。
  • 但归一化的Muscle空间本身就对动画信息进行了精简,自身内存占用相比Generic已经降低了不少,如果需要继续降低,可以尝试提高压缩选项下的Error值。

Animator会把所有状态的AnimationClip加载到内存,有什么好的办法可以动态加载?

  • Animator Controller结构不需要改变,但动画需要变化。比如随着人物等级或技能升级,同种的攻击动作随着变化等。该种需求可以通过AnimatorOverrideController来进行完成,即按需加载新的AnimationClip,然后替换AnimatorOverrideController中相应的AnimationClip即可。目前,Unity的AnimatorOverriderController不仅可以进行单个替换,同时也可以ApplyOverrides成组替换;
  • Animator Controller结构需要改变,类似于Animation老版本动画的AddClip功能。这种需求需要替换AnimatorController,研发团队可以在动态加载AnimationClip的同时,动态加载相应的Animator Controller,然后进行替换即可。

为什么Transform.设置Parent会触发Animator的初始化吗?Parent是等于null的。

  • 把 Parent 设为null,相当于把这个 GameObject 变为根节点。如果在设置之前这个 GameObject 本身是激活状态,但其的父节点是未激活状态,那么设置之后,相当于把这个GameObject 激活。而激活GameObject 时就会触发 Animator.Initialize 等操作了。

物理

在Unity 的 Profiler里,有些记录右边会有Warning的个数信息,请问这个是否影响性能,或者是否有必要修改呢?

image

  • 这种字符的出现很可能会导致后续的物理更新出现较大的性能开销,包含在Physics.Simulate/Processing中。
  • 针对这个图是受限于Unity版本中的PhysX在移动静态碰撞体是开销较高的问题(虽然文档中说5.x下已经解决,但我们确实发现在5.x的项目中该项仍然存在)。
  • 建议给需要移动的Collider加上RigidBody并勾选Is Kinematic复选框,从而将其变为动态碰撞体。对于不移动的物体,则直接将模型的Apply Root Motion选项进行关闭,从而直接省去Static Collider.Move的性能开销。

性能综合

我们设置了TargetFrameRate为30,想避免过高的耗电和发热,请问这样做是合理的么?

  • 这种设置仅在帧率本身很好的情况下,才会起到减少耗电和发热的作用。但如果本身游戏已经较为卡顿,那么该设置方法意义不大。对于耗电和发热,研发团队需从CPU、GPU和IO入手,尽可能降低这三方面的负载压力。

项目渲染中Material.SetPassFast的开销高

  • Material.SetPassFast是Unity引擎在渲染过程中Material的轮循切换开销,一般在Unity5.0~Unity5.3版本中出现。它的开销主要分为两种:
    • Shader.CreateGPUProgram峰值开销:这种情况主要出现在Shader第一次渲染时。在Unity5.0以后,引擎为了避免Shader加载时过高的CPU峰值出现,已经将Shader.Parse和Shader.CreateGPUProgram两种操作分开执行,前者在Shader加载时,后者在Shader第一次渲染时。
    • 渲染状态切换开销:这种情况是几乎每一帧都发生的,有渲染的地方就会有Material的切换。从问题图中可以看出,在运行的16000帧中,Material.SetPassFast一共被调用137万次。这里可以认为几乎全部是渲染时Material的切换操作。因此,该项较高的主要原因还是材质切换操作过多所致。所以,建议研发团队在报告中的详细材质页面查看是否有过多“冗余”的材质出现,如有则尽可能降低材质的使用冗余度。

渲染

将需要的Shader打到一个AssetBundle包中(包含一个关联了所有Shader的Shader Variants),分别用Shader.WarmupAllShaders和ShaderVariantCollection.WarmUp两种方式进行预加载,后者耗时更少。

  • 根据官方的文档的描述,确实是ShaderVariantCollection的效率更高,详见: https://docs.unity3d.com/Manual/OptimizingShaderLoadTime.html
  • 因为在ShaderVariantCollection中,是可以给每个Shader添加指定的Keyword的,ShaderVariantCollection.WarmUp的调用只会对ShaderVariantCollection中指定的Keyword进行Warmup操作;而Shader.WarmupAllShaders则是对所有的Keyword全部进行Warmup操作(其中大多数很可能都不会用到)。
  • 因此在Shader.WarmupAllShaders的文档中也提到,建议使用ShaderVariantCollection.Warmup来进行细粒度的Warmup操作,避免大量多余的Keyword被Warmup,造成严重的卡顿,大家可以参考下文: https://docs.unity3d.com/ScriptReference/Shader.WarmupAllShaders.html

在Unity 5.6版本中如何解决预渲染缺少高光的问题?该版本中光照预渲染Directional Mode选项中少了Directional Specular选项,渲染出来的效果场景缺少高光。

  • 如果需要在使用Lightmap时渲染高光,替代方案是采用Mix Lights模式下的Shadow Mask以及Distance Shadow Mask选项。即将场景中的Directional Light改成Mix Lighting类型,并且在Lighting Mode选Shadow Mask或者Distance Shadow Mask。
  • 其原理是:LIghtmap中仅仅存储indirect的光照,而direct光照是实时计算的,所以包括高光、阴影等都可以是实时的(阴影也可以是预计算好的)。这样做的好处是给Lightmap光照一定的灵活度,原来的Lightmap是完全静态的,现在是部分静态(direct的实时,indirect静态)。

Unity 5 的 Shader Variant Collection 功能

  • 经过测试,在较新的版本中(如Unity 5.5.3),将ShaderVariantCollection与Shader打包在相同的AssetBundle中后,其中会包含该Shader在ShaderVariantCollection中指定的Variant。
使用 Shader 变体之后,Shader 是否还能走资源更新?抑或 Shader 不推荐走资源更新?
  • 使用 ShaderVariantCollection后依然可以进行资源更新(通过更新AssetBundle,来更新Shader的实现或者ShaderVariantCollection中包含的Variant)。
Shader 变体和 Shader Always Include 的主要区别是什么?二者对内存和帧率影响如何?
  • ShaderVariantCollection与Always Included Shaders的区别主要在于打包时所包含的Variant。Always Included Shaders中的Shader,其所有的Variant都会被包含,好处是,理论上不会出现Variant丢失的情况;坏处是,会导致更大的发布包以及额外的内存占用,而影响最大的是手动进行Warmup时的耗时以及ShaderLab的内存占用。因此一般来说,对于Variant特别多的Shader(如Standard Shader),并不推荐放入Always Included Shaders中。
在 Unity 5 较早的版本中,Shader 变体功能似乎有一些Bug,现在是否可靠?
  • 目前即使是较新的版本,其可靠性我们也并不能确保,依然建议在使用前进行一些测试来验证。

  • ==PS: 需要关注的问题==