Unity 面试题汇总(五)性能优化知识点相关

Unity 面试题汇总(五)性能优化知识点相关

目录

Unity 面试题汇总(五)性能优化知识点相关

1、资源分离打包与加载

2、贴图透明通道分离,压缩格式设为ETC/PVRTC

3、关闭贴图的读写选项

4、减少场景中的GameObject数量

5、图集

6、降低贴图素材分辨率

7、界面的延迟加载和定时卸载策略

8、避免频繁调用GameObject.SetActive

9、移动端性能优化心得

10、逻辑代码方法

11、GPU Instancing


1、资源分离打包与加载

  游戏中会有很多地方使用同一份资源。比如,有些界面共用同一份字体、同一张图集,有些场景共用同一张贴图,有些怪物使用同一个Animator,等等。在制作游戏安装包时将这些公用资源从其它资源中分离出来,单独打包。比如若资源A和B都引用了资源C,则将C分离出来单独打一个bundle。在游戏运行时,如果要加载A,则先加载C;之后如果要加载B,因为C的实例已经在内存,所以只要直接加载B,让B指向C即可。如果打包时不将C从A和B分离出来,那么A的包里会有一份C,B的包里也会有一份C,冗余的C会将安装包撑大;并且在运行时,如果A和B都加载进内存,内存里就会有两个C实例,增大了内存占用。

  资源分离打包与加载是最有效的减小安装包体积与运行时内存占用的手段。一般打包粒度越细,这两个指标就越小;而且当两个renderQueue相邻的DrawCall使用了相同的贴图、材质和shader实例时,这两个DrawCall就可以合并。但打包也并不是越细就越好。如果运行时要同时加载大量小bundle,那么加载速度将会非常慢——时间都浪费在协程之间的调度和多批次的小I/O上了;而且DrawCall合并不见得会提高性能,有时反而会降低性能,后文会提到。因此需要有策略地控制打包粒度。一般只字体和贴图这种体积较大的公用资源。

  可以用AssetDatabase.GetDependencies得知一份资源使用了哪些其它资源。

2、贴图透明通道分离,压缩格式设为ETC/PVRTC

  最初我们使用了DXT5作为贴图压缩格式,希望能减小贴图的内存占用,但很快发现移动平台的显卡是不支持的。因此对于一张1024x1024大小的RGBA32贴图,虽然DXT5可将它从4MB压缩到1MB,但系统将它送进显卡之前,会先用CPU在内存里将它解压成4MB的RGBA32格式(软件解压),然后再将这4MB送进显存。于是在这段时间里,这张贴图就占用了5MB内存和4MB显存;而移动平台往往没有独立显存,需要从内存里抠一块作为显存,于是原以为只占1MB内存的贴图实际却占了9MB!

所有不支持硬件解压的压缩格式都有这个问题。经过一番调研,我们发现安卓上硬件支持最广泛的格式是ETC,苹果上则是PVRTC。但这两种格式都是不带透明(Alpha)通道的。因此我们将每张原始贴图的透明通道都分离了出来,写进另一张贴图的红色通道里。这两张贴图都采用ETC/PVRTC压缩。渲染的时候,将两张贴图都送进显存。同时我们修改了NGUI的shader,在渲染时将第二张贴图的红色通道写到第一张贴图的透明通道里,恢复原来的颜色:

fixed4 frag (v2f i) : COLOR  

    fixed4 col;  

    col.rgb = tex2D(_MainTex, i.texcoord).rgb;  

    col.a = tex2D(_AlphaTex, i.texcoord).r;  

    return col * i.color;  

fixed4 frag (v2f i) : COLOR

{

    fixed4 col;

    col.rgb = tex2D(_MainTex, i.texcoord).rgb;

    col.a = tex2D(_AlphaTex, i.texcoord).r;

    return col * i.color;

}

这样,一张4MB的1024x1024大小的RGBA32原始贴图,会被分离并压缩成两张0.5MB的ETC/PVRTC贴图(我们用的是ETC/PVRTC 4 bits)。它们渲染时的内存占用则是2x0.5+2x0.5=2MB。

3、关闭贴图的读写选项

Unity中导入的每张贴图都有一个启用可读可写(Read/Write Enabled)的开关,对应的程序参数是TextureImporter.isReadable。选中贴图后可在Import Setting选项卡中看到这个开关。只有打开这个开关,才可以对贴图使用Texture2D.GetPixel,读取或改写贴图资源的像素,但这就需要系统在内存里保留一份贴图的拷贝,以供CPU访问。一般游戏运行时不会有这样的需求,因此我们对所有贴图都关闭了这个开关,只在编辑中做贴图导入后处理(比如对原始贴图分离透明通道)时打开它。这样,上文提到的1024x1024大小的贴图,其运行时的2MB内存占用又可以少一半,减小到1MB。

4、减少场景中的GameObject数量

  有一次我们将场景中的GameObject数量减少了近2万个,游戏在iPhone 3S上的内存占用立马减了20MB。这些GameObject虽然基本是在隐藏状态(activeInHierarchy为false),但仍然会占用不少内存。这些GameObject身上还挂载了不少脚本,每个GameObject中的每个脚本都要实例化,又是一比不菲的内存占用。因此后来我们规定场景中的GameObject数量不得超过1万,并且将GameObject数量列为每周版本的性能监测指标。

5、图集

  整理图集的主要目的是节省运行时内存(虽然有时也能起到合并DrawCall的作用)。从这个角度讲,显示一个界面时送进显存的图集尺寸之和是越小越好。一般有如下方法可以帮助我们做到这点:

  1)在界面设计上,尽量让美术将控件设计为可以做九宫格拉伸,即UISprite的类型为Sliced。这样美术就可以只切出一张小图,我们在Unity中将它拉大。当然,一个控件做九宫格也就意味着其顶点数量从4个增加到至少16个(九宫格的中心格子采用Tiled做平铺类型的话,顶点数会更多),构建DrawCall的开销会更大(见第6点),但一般只要DrawCall安排合理(同样见第6点)就不会有问题。

  2)同样是在界面设计上,尽量让美术将图案设计成对称的形式。这样切图的时候,美术就可以只切一部分,我们在Unity中将完整的图案拼出来。比如对一个圆形图案,美术可以只切出四分之一;对一张脸,美术可以只切出一半。不过,与第1)点类似,这个方法同样有其它性能代价——一个图案所对应的顶点数和GameObject数量都增多了。第4点已经提到,GameObject数量的增多有时也会显著占用更多内存。因此一般只对尺寸较大的图案采用这个方法。

  3)确保不要让不必要的贴图素材驻留内存,更不要在渲染时将无关的贴图素材送进显存。为此需要将图集按照界面分开,一般一张图集只放一个界面的素材,一个界面中的UISprite也不要使用别的界面的图集。假设界面A和界面B上都有一个小小的一模一样的金币图标,不要因为在制作时贪图方便,就让界面A的UISprite直接引用界面B中的金币素材;否则界面A显示的时候,会将整个界面B的图集也送进显存,而且只要A还在内存中,B的图集也会驻留内存。对于这种情况,应该在A和B的图集中各放一个一模一样的金币图标,A中的UISprite只使用A的图集,B中的UISprite只使用B的图集。

  不过,如果两个界面之间存在大量相同的素材,那么这两个界面就可以共用同一张图集。这样可以减少所有界面的总内存占用量。具体操作时需要根据美术的设计进行权衡。一般界面之间相同的通用的素材越多,程序的内存负担就越小。但界面之间相同的东西太多的话,美术效果可能就不生动,这是美术和程序之间又一个需要寻求平衡的地方。

  另外,数量庞大的图标资源(如物品图标)不要做在图集里,而应该采用UITexture。

4)减少图集中的空白地方。图集中完全透明的像素和不透名的像素所占的内存空间其实是一样的。因此在素材量不变的情况下,要尽量减少图集中的空白。有时一张1024x1024的图集中,素材所占的面积还没超过一半,这时可以考虑将这张图集切成两张512x512的图集。(有人会问为什么不能做成一张1024x512的图集,这是因为iOS平台似乎要求送进显存的贴图一定是方形。)当然,两张不同图集的DrawCall是无法合并的,但这并不是什么问题(见第6点)。

  应该说,图集的整理在具体操作时并没有一成不变的标准,很多时候需要权衡利弊来最终决定如何整理,因为不管哪种措施都会有别的性能代价。

6、降低贴图素材分辨率

  这一招说白了其实就是减小贴图素材的尺寸。比如对一张在原画里尺寸是100x80的,我们将它导入Unity后会把它缩小到50x40,即缩小两倍。游戏实际使用的是缩小后的贴图。不过这一招是必然会显著降低美术品质的,美术立马会发现画面变得更模糊,因此一般不到程序撑不住的时候不会采用。

7、界面的延迟加载和定时卸载策略

  如果一些界面的重要性较低,并且不常被使用,可以等到界面需要打开显示的时候才从bundle加载资源,并且在关闭时将卸载出内存,或者等过一段时间再卸载。不过这个方法有两个代价:一是会影响体验,玩家要求打开界面时,界面的显示会有延迟;二是更容易出bug,上层写逻辑时要考虑异步情况,当程序员要访问一个界面时,这个界面未必会在内存里。因此目前为止我们仍未实施该方案。目前只是进入一个新场景时,卸载上一个场景用到但新场景不会用到的界面。

  以上的9个方法中,4、5、6需要在一定程度上从策划和美术的角度考虑问题,并且需要持续保持监控以维护优化状态(因为在设计上总是会有新界面的需求或改动老界面的需求);其它都是一劳永逸的解决方案,只要实施稳定后,就不需要再在上面花费精力。不过2和8都是会降低美术品质的方法,尤其是8。如果美术对品质的降低程度实在忍不了的话,也可能不会允许采用这两个方法。

8、避免频繁调用GameObject.SetActive

我们游戏的某些逻辑会在一帧内频繁调用GameObject.SetActive,显示或隐藏一些对象,数量达到一百多次之多。这类操作的CPU开销很大(尤其是NGUI的UIWidget在激活的时候会做很多初始化工作),而且会触发大量GC。后来我们改变了显示和隐藏对象的方法——让对象一直保持激活状态(activeInHierarchy为true),而原来的SetActive(false)改为将对象移到屏幕外,SetActive(true)改为将对象移回屏幕内。这样性能就好多了。
 

9、移动端性能优化心得

CPU端性能优化

  • 逻辑和表现尽可能分离开,这样逻辑层的更新频率可以适当降低些.
  • 对于一些热点函数,如mmo的实体更新、实例化,使用分帧处理,分摊单帧时间消耗.
  • 做好同屏实体数量、特效数量、距离显隐等优化.
  • 完善日志输出,避免没必要的日志输出,同时警惕日志字符串拼接.
  • 使用骨骼烘焙 + GPUSkinning + Instance 降低CPU蒙皮骨骼消耗和drawcall.
  • 开启模型的Optimize GameObjects减少节点数量和蒙皮更新消耗.
  • UI拼预制做好动静分离,对于像血条名字这种频繁变动的ui,做好适当的分组.
  • 减少C#和lua的频繁交互,尽量精简两者传递的参数结构.
  • 使用stringbuilder优化字符串拼接的gc问题.
  • 删除非必要的脚本功能函数,特别是Update/LateUpdate类高频执行函数,因为会产生C++到C#层的调用开销. 对于Update里需要用到的组件、节点等提前Cache好.
  • 场景里频繁使用的资源或数据结构做好资源复用和对象池.
  • 对于频繁显示隐藏的UI,可以先移出到屏幕外,如果长时间不显示再进行Deactive.
  • 合理拆分UI图集,区分共用图集和非共用图集,共用图集可以常驻内存,非共用图集优先按功能分类,避免资源冗余.
  • 使用IL2CPP, 编译成C++版本能极大的提升整体性能.
  • 避免直接使用Material.Setxxx/Getxxx 等调用,这些调用会触发材质实例化消耗,可以考虑使用 SharedMaterial / MaterialPropertyBlock代替.
  • 合并Shader里的Uniform变量.

GPU端性能优

  • 合理规划好渲染顺序,避免不必要的overdraw,如:地形(容易被其他物件遮挡)、天空盒放到较后渲染.
  • 分辨率缩放,对于填充率出现瓶颈时,这个是最简单高效的.
  • 避免使用GrabPass抓屏,不是所有硬件都支持,加之数据回拷和没法控制分辨率性能很差,可考虑使用CommandBuffer.blit去优化.
  • 控制好地形的Blend层数,控制在4层以内,考虑到地形一般屏占面积大、贴图采样次数多,对于中低画质考虑不用normalmap.
  • 做好物件、树、角色的LOD.
  • 避免使用RenderWithShader类方式来定制DepthTexture,可以考虑Camera的 public void SetTargetBuffers(RenderBuffer colorBuffer, RenderBuffer depthBuffer);进行优化.
  • 检查Shader的VertexInput 和 VertexOutput是否存在冗余数据.如:顶点色、多套UV.
  • 警惕项目里非必要的双面材质,对于需要局部双面的地方通过加面解决.
  • Shader里使用fixed、half代替float,理论上除position、uv、一些涉及depth相关计算使用float外,其他都应该使用fixed(主要是颜色值)、half.
  • 对于角色皮肤这种不是特别明显的效果,考虑使用预积分这种低成本的方案.
  • 对于frag里的计算过程,如果可以抽出来放到CPU应用层、顶点阶段的优先放这里计算. 需要注意放到顶点阶段引起的平滑过渡问题. 如: eyeVec导致高光过渡问题.
  • 镜面反射类效果避免使用反射相机+RT的实现,考虑使用SSR、CubeMap类实现.
  • 避免使用实时阴影,如若使用要合理控制下分辨率和阴影距离. 考虑使用Projector.
  • 使用统一的后处理框架代替多个Image Effect,可以共用模糊函数,减少blit操作. 另外Unity自带的Postprocessing V2 支持Volume,性能还是不错的.
  • Shader里避免使用分支、循环,sin、tan、pow、log等复杂数学运算.
  • Unity自带的遮挡剔除因为CPU消耗和内存占用较高,加之不能Instancing,不太适合移动平台,可以考虑静态预计算(缺点是不支持动态物体)、Hi-Z等优化方案.
  • 减少alpha test材质的使用,如若使用注意减小面积、控制渲染顺序.

内存优化

  • 警惕配置表的内存占用.
  • 检查ShaderLab内存占用:
  • 避免使用Standard材质,做好相应的variant skip.
  • 排查项目冗余的Shader.
  • 使用shader_feature替代multi_compile,这样只会收集项目里真正使用的变体组合,避免变体翻倍.
  • 检查纹理资源的尺寸、格式、压缩方式、mipmap、Read & Write选项使用是否合理.
  • 检查Mesh资源的Read & Write选项、顶点属性使用是否合理.
  • 代码级别的检查,如Cache预分配空间、容器的Capacity、GC等.
  • 使用Profiler定位下GC,特别是Update类函数里的. 如:字符串拼接、滥用容器等.
  • 合理控制RenderTexture的尺寸.
  • 优化动画Animation的压缩方式、浮点精度、去除里面的Scale曲线数据.
  • 减少场景GameObject节点的数量,最好支持工具监控.

10、逻辑代码方法

下面列举一些容易产生堆内存的函数

Unity API:

  • Debug.Log
  • AssetBundle.LoadAsset
  • Object.Instantiate/GameObejct.SetActive
  • Object.name
  • GameObject.AddComponent
  • ParticleSystem.Play/Stop/...(不指定某个粒子系统调用,就是相当于GetComponentInChildrens差不多)
  • Physics.Raycast

Plugins:

  • UIPanel.LateUpdate
  • LuaInterface.LuaDLL.lua_tostring
  • Protobuff.Serializer.Deserialize

System:

  • System.Delegate.Combine
  • Foreach
  • string.Concat/Split/ToLower

11、GPU Instancing

参考网址:Unity - Manual: GPU instancing

Use GPU Instancing to draw (or render) multiple copies of the same Mesh
at once, using a small number of draw calls. This is useful for drawing objects such as buildings, trees, grass, or other things that appear repeatedly in a Scene
.

GPU Instancing only renders identical Meshes with each draw call, but each instance can have different parameters (for example, color or scale) to add variation and reduce the appearance of repetition.

GPU Instancing can reduce the number of draw calls used per Scene. This significantly improves the rendering performance of your project.

使用 GPU Instancing 绘制(或渲染)多个副本 一次,使用少量的绘制调用。这对于绘制建筑物、树木、草地或其他重复出现的物体等物体很有用场景
.

GPU 实例化仅在每次绘制调用时渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少重复的出现。

GPU 实例化可以减少每个场景使用的绘制调用数量。这显着提高了项目的渲染性能。

Adding instancing to your Materials (将实例添加到您的材质中 

要在材质上启用 GPU 实例化,请在 项目窗口,并且在 Inspector,勾选启用实例化复选框。

 

 

 

当您使用 GPU 实例化时,以下限制适用:

  • Unity 自动选择 MeshRenderer 组件并Graphics.DrawMesh调用实例化。请注意,不支持SkinnedMeshRenderer

  • Unity 仅对在单个 GPU 实例化绘制调用中共享相同网格和相同材质的游戏对象进行批处理。使用少量网格和材料以获得更好的实例化效率。要创建变体,请修改您的着色器脚本
     添加每个实例的数据(请参阅下一节以了解更多信息)。

您还可以使用调用Graphics.DrawMeshInstancedGraphics.DrawMeshInstancedIndirect从脚本执行 GPU 实例化。

GPU 实例可在以下平台和 API 上使用:

  • Windows 上的DirectX 11DirectX 12

  • Windows、macOS、Linux 和 Android 上的OpenGL Core 4.1+/ES3.0+

  • macOS 和 iOS 上的Metal

  • Windows、Linux 和 Android 上的Vulkan

  • PlayStation 4Xbox One

  • WebGL
     (需要 WebGL 2.0 API)

Guess you like

Origin blog.csdn.net/u014361280/article/details/121534764