本系列探讨Unity中的一些性能优化要点,非常琐碎,会整理出所见到的细节知识和一些相关联的衍生知识点。

 

本章是siki学院课程UGUI优化篇的学习笔记,想看视频的可以到官网去查看更细节的要点。

 

1,基础名词介绍

Unity下所有游戏物体都是通过网格进行绘制的,wireframe模式下可以查看;

drawcall:是一个由CPU发起,GPU接收的命令,这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息;

填充率,overdraw模式下可以查看;

batch和合批:

batch,是大部分引擎提高渲染效率的方法,基本原理就是通过将一些渲染状态一致的物体合成一个大物体,一次提交给gpu进行绘制,如果不batch的话,就要提交给很多次,这可以显著的节省drawcall,实际上这主要节省了cpu的时间,cpu从提交多次到提交一次,对gpu来说也不用多次切换渲染状态。当然能batch的前提一定是渲染状态一致的一组物体

 

2,Canvas和Graphic的关联

Cavas:C++实现内部的具体方法,有一个非常重要的事件  public static event WillRenderCanvases willRenderCanvases ;

Canvas会将当前所有子物体能合批的合批,发送渲染指令到Unity的图形渲染系统,通过子物体的CanvsRenderer组件获取它下面子物体的几何图形并描绘出来,对所有子物体进行整理并批处理的过程称为Rebatch;

所有能显示出来的组件都是继承自Graphic,是所有可以绘制出来的UI组件基类,继承自接口ICanvasElement,通常所说的Rebuild也是来源于这个接口;

在进行批处理之后,所有具有脏标记(Mesh被修改)的UI组件就会执行Rebuild重建;

Graphic类中有SetAllDirty方法会设置包括布局,材质,顶点的脏标记

 

3,重建部分重要的类

Graphic类,它的子类MaskableGraphic;

MaskableGraphic有遮挡是因为它继承了接口IMaskable,能被Mask组件遮罩剔除;

LayoutRebuilder类,重要方法是MarkLayoutRootdForRebuild,对组件是否需要重绘进行注册,如果注册了那么会加入到某个队列中;

能排列的继承自接口ILayoutElement,同时控制物体的位置和大小;

通常情况下避免使用自动排序组件,自动排序导致的重建消耗非常高;

CanvasUpdateRegistry,负责Canvas更新的注册,是否需要重建都是通过这里进行调用,会在构造函数里面绑定Canvas的事件willRenderCanvases,在PerformUpdate里面进行重建的主要内容,通知在Rebuild的队列中的物体进行重建

 

4,两种遮罩Mask和RectMask2D的区别

RectMask2D实现遮罩的功能是因为实现了IClipper接口,Mask实现遮罩是通过GetModifiedMaterial方法,通过模板缓存Stencil

 

5,深度测试

颜色缓冲区和深度缓冲区;

透明物体的渲染,增加了填充率OverDraw;

UI物体都会放在透明物体队列的渲染中,所以会存在OverDraw的问题

 

6,网格重建部分的执行逻辑

UGUI的网格重建涉及到两部分,第一个是合批部分的Rebatch,是Canvas重新合批时造成的消耗,只要Canvas下的任何一个物体发生了改变,都会触发网格重建,使得每一个子物体都会进行重绘,所以一大堆的UI元素都处于同一个Canvas下时,如果子物体频繁发生更改,那么会造成不小的性能消耗;

第二个是针对单个物体的Rebuild,在Canvas类中有一个BuildBatch方法,在这个方法中计算出哪些元素需要Rebuild;

PerformUpdate的主要执行逻辑:

对Layout部分的重建,Mask部分的裁剪,Graphic里面需要重建的元素队列的重建(顶点和材质);

直接控制物体的显隐gameObject.SetActive会造成额外的性能消耗的原因是因为在Graphic的OnEnable方法中SetAllDirty了,设置了所有的脏标记,在OnDiable中对布局进行了Rebuild,LayoutRebuilder.MarkLayoutForRebuild

 

7,UI射线部分执行逻辑

主要类是GraphicRaycaster,方法为Raycast,获取当前Canvas下的所有继承自Graphic的组件,获取事件相机,获取Canvas的TargetDisplay,然后获得当前Canvas的宽高,计算屏幕坐标转换来的视口坐标,判断这个坐标有没有在0到1的有效范围内,之后获取GraphicRaycaster的BlockingMask,获得阻塞对象的参数,作为当前射线击中的距离,获得到所有能击中的物体后,进入射线的具体逻辑,遍历每个被击中的物体,首先判断这个物体有没有被绘制(depth=-1),是否能响应射线(raycastTarget),是否能被遮罩(canvasRender.IsCull),当前这个点击的点是否在当前物体的绘制区域内,Z轴是否已经超出了相机的最远面,然后进入Graphic的RayCast方法,看当前这个组件是否为ICanvasRaycastFilter,符合条件之后进行深度测试,将返回的Graphic添加到一个排序列表中根据深度进行排序,回到调用方法中,之后首先判断这个物体是否为一个反面的目标,过滤掉一些在相机背后渲染的物体,并且判断距离小于击中距离,才能视为真正被击中了,加入到结果数组中

 

8,UGUI合批规则简介

遍历所有UI物体,判断UI的深度Depth,Material_ID,Texture_ID,RenderOrder,剔除掉depth为-1的物体,对UI进行排序,然后如果此时有UI的Material_ID和Texture_ID相同,并且它们必须是紧挨着的顺序排列的,  那么就可以进行合批,传入排序好的列表,在合批时,依次确定批次号

 

源码剖析可以看这篇博客:


 

unity cube 6个面_unity

depth的确定规则,最先被渲染的物体的depth为0,然后如果后面的物体遮挡住了前一个物体(Mesh有遮挡),并且无法完成合批,那么这个物体的depth等于前面物体中深度最大值+1,能完成合批那么等于被合批队列的depth

 

 

FrameDebugger查看工具:

unity cube 6个面_unity cube 6个面_02

 

Profier查看工具:

unity cube 6个面_UI_03

 

9,额外的dc

相机自己会产生一个dc,Maks本身会造成2个dc,第1个是最开始设置模板缓存的时候,在当前像素的值上设置一个临时数据(0、1),1为绘制,0不绘制,第2个是遍历完它下面所有子物体后,还原模板缓存的时候

 

10,Mask为什么不能合批

在Mask类的GetModifierMaterial方法中,处理模板缓存的过程中,

unity cube 6个面_UI_04

有为它设置一个另外的Material,所以Mask不能和其他的UI物体进行合批,但是相同Texture且Depth相同的Mask之间在满足合批的条件下可以合批

 

11,Mask的一些注意要点

unity cube 6个面_unity cube 6个面_05

Mask下的子物体无法和外部物体进行合批,Maks会影响深度的计算,即便被Mask完全裁剪的物体也依然会占据原本的dc

 

 

unity cube 6个面_UI_06

Mask下的子物体之间在满足合批条件的前提下可以正常进行合批

 

 

unity cube 6个面_UI_07

unity cube 6个面_UI_08

不同的mask之间可以进行合批,且它们下面的子物体也可以进行合批;

 

unity cube 6个面_UI_09

即便被Mask裁剪了,被裁剪的物体也依然会有dc,且遵从之前的深度计算规则,所以如果有能进行合批的Mask,尽量不要让他们的子物体之间有重叠的部分

 

12,RectMask2D

本身并不占据dc,原理是利用被裁剪的子物体进行遮罩,给子物体传入自身的RectTransform所在的区域,从而获得实际裁剪的区域,主要函数为PerformClipping,如果物体被完全裁剪掉了,那么这个物体就不再占用dc,且顶点和面片都将不再进行绘制;

具有相同子物体的同层级的RectMask2D不能进行合批,但各个RectMask2D下面的子物体能分别进行合批操作;

unity cube 6个面_UI_10

 

RectMask2D上的Image可以进行合批;

unity cube 6个面_UI_11

 

13,RectMask2d和Mask

如果需要有多个遮罩物体,且这些物体的同层级子物体都能进行合批,那么可以优先考虑使用Msk,如果需要遮罩的物体较少,或者遮罩下面的同层级子物体大多都不能进行合批,那么有限考虑使用RectMask2D

 

14,填充率

即便某个物体的alpha设置为了0,仍然是有填充率的;

尽量减少UI物体之间的覆盖,减少填充率

 

15,Shadow和Outline组件

会大大增加Text的顶点数和面片数,且效果差强人意,可以考虑使用TextMeshPro的描边

 

16,Unity的天空盒

会有非常多的顶点数和面片数,如果场景中只有2D物体那么需要去掉天空盒子;

如果需要渲染3D物体,那么可以分开两个相机进行渲染

 

17,Image的平铺模式会有额外的顶点数

unity cube 6个面_UI_12

使用RawImage的平铺会减少这部分额外的顶点数:

unity cube 6个面_unity cube 6个面_13

 

18,Canva下的PixelPerfect

一般不要去勾选,如果下面有类似ScrollView的组件在进行滑动时会频率的触发像素点的绘制矫正,造成额外的性能消耗

unity cube 6个面_Mask_14

 

19,RaycastTarget

无论时文本还是图片,如果不需要UI事件响应,不要勾选RaycastTarget

unity cube 6个面_Mask_15

 

20,UI动静分离

如果某个Canvas下面的UI物体非常多,且仅有少部分物体需要属性更改,可以考虑加入一个子的Canvas,将这部分经常变化的物体放入子Canvas下,但是需要注意的是子Canvas会打断合批,自行权衡

unity cube 6个面_UI_16

 

21,UI组件的显隐

如果需要控制UI物体的显隐,如果使用gameObject.SetActive,见上文会造成不小的消耗,那么如果使用Color来调整,会引起它的重建,可以考虑通过材质球的_Color属性进行调整,是不会引起重建的,如果只是控制单个物体的显隐,推荐使用CanvasRenderer的CullTransparentMesh,如果需要控制一组物体的显隐,推荐使用CanvasGroup的Alpha属性

unity cube 6个面_unity cube 6个面_17

unity cube 6个面_Graphic_18

unity cube 6个面_unity cube 6个面_19

 

22,不要开启Text的BestFit属性

Text组件会在Unity内部缓存不同字内容的图元,如果开启了这个属性,就会生成许多内容一样但是形式(主要是大小)不同的图元

 

23,打断合批的操作

修改UI的positionZ不为默认值0,都会使这个UI退出合批

unity cube 6个面_Graphic_20

如下图即便发生了覆盖也没有发生深度值depth的修改,说明一旦修改UI的position的Z轴的值不为默认值都会使这个UI推出合批,并且打断合批

unity cube 6个面_unity cube 6个面_21

 

 

修改UI的Rotation的XY也会打断合批,但是如果修改后的值是180的倍数(只要保证修改后能和其他能合批的物体处在同一个平面上),都能进行合批,如果是90的倍数都已经不进行渲染了,其他的角度都会增加额外的dc,修改Z不会

unity cube 6个面_Graphic_22

 

需要注意的是如果此时将各个UI物体拆分开,那么无论是上面的修改posZ还是修改旋转角度xy都不会打断合批

unity cube 6个面_Graphic_23

 

 

24,图集存在的意义

保证不同UI上的TextureID相同,尽可能保证减少dc

 

Unity图集开启:

unity cube 6个面_Mask_24

 

25,Unity的新版(2017之后)和旧版(2017之前)图集

旧版图集:

unity cube 6个面_Graphic_25

多出一个PackingTag,而且放在Resource目录下的图片不能打出图集,自动打出图集,会根据图片的格式,分成不同的类别(例如带Alpha通道和不带Alpha通道的)

 

图集查看方式:

unity cube 6个面_UI_26

 

新版图集:

没有PackingTag,需要手动创建SpriteAtlas

unity cube 6个面_unity cube 6个面_27

新版图集的一些属性介绍:

Type: Master,Varient

Packing: Allow Rotation,在打包图集时是否允许旋转内部的图片,避免产生空隙,节省空间

TightPacking: 紧密打包,节省空间

 

Objects for packing:可以允许打入这个图集的图片或者文件夹

PackPreview:预览

unity cube 6个面_UI_28

打入这个图集内的所有图片都会设置成统一的格式

 

新版Variant图集的使用:

相当于一个子图集,会继承自主图集的所有属性,主要应用在设置不同分辨率下的图集,调整Variant Scale,针对不同机型选择较为接近的不同的图集,提高运行效率,节省无用空间

unity cube 6个面_unity_29

 

26,打图集的基本规则

(1),尽可能把同一个界面下所需要加载的Sprite都放入同一个文件夹下,打入同一个图集中,如果是各个界面通用的资源,可以打入公用的图集中

(2),打入图集中的图尽可能都是小的Icon,不必要把大图放入图集中

 

27,颜色通道的相关知识

所有的图片都会由RGB三种颜色按照比例调配而成,图片能否设置透明取决于它是否有Alpha通道(灰度通道)

RGBA32:32代表RGBA四个通道加起来一共32位

 

28,纹理压缩格式

图片格式jpg(有损压缩无透明),png(无损压缩有透明)

无论哪种格式的图片在导入时Unity都会将它转化为纹理格式,可被GPU直接读取

不同的GPU针对不同的纹理压缩格式是否可解压也是不同的,所以如果某个平台无法解压纹理时,Unity会将其转化为RGBA的格式,但是因为RGBA的格式占空间大,所以不再一开始使用这种格式

unity cube 6个面_Mask_30

 

内存计算方式:

unity cube 6个面_UI_31

计算方式:1024*1024*4Byte = 4M,44Byte是因为压缩格式是RGBA32bit,每一个像素占了32bit,1Byte = 8 bit,所以一个像素占了4Byte

unity cube 6个面_unity_32

如果选择的是像素块呢?(ASTC)

1block = 16Byte,那么1像素占据的空间就是1Byte,下图应用后的空间大小就是1M

unity cube 6个面_unity_33

 

29,安卓平台下的压缩格式

基于感知质量设计的有损算法,依据是人眼对于不同亮度的感受的不同

ETC(不支持透明通道,支持OpenGl2.0的设备上)和ETC2(压缩格式质量稍好,且支持透明通道,支持OpenGl3.0的平台)

unity cube 6个面_Graphic_34

 

30,Unity的二次压缩格式

带Crunched的指的是Unity会在之前的压缩算法基础上再次进行二次压缩,可以使得IO加载的文件大小更小,包体更小,加载速度更快,缺点是运行时需要二次解压,缺点时压缩时间更长

unity cube 6个面_UI_35

勾上这个选项后会在所有能支持的平台上都进行二次压缩

unity cube 6个面_unity cube 6个面_36

 

31,Ios的压缩格式的选择

PVRTC或者ASTC

总结:在图片质量优先的前提下,选择最小空间,最优性能的压缩格式

 

32,图片的格式要求

ETC压缩的要求时图片的宽高必须是4的倍数,且只有POT(power of two)的Texture才能满足要求,宽高必须是2的幂次方

ETC2是宽高必须是4的倍数

PVRTC压缩的要求是图片的宽高必须是2的幂次方,且必须是正方形,宽高相等

可以查看Unity的警告提示

 

33,图片的通道分离

对于不支持透明通道的压缩格式,如果想要支持透明度,除了需要支持RGB通道的图外,需要额外再加一张支持Alpha通道的图

可以把RGBA的图分离成两张图,可以节省内存,但是如果是ETC的图,再进行分离的话反而额外增加了内存

如果已经是ETC2能满足需求了,就不要使用通道分离

IOS下不建议使用通道分离

unity cube 6个面_UI_37

 

Unity中使用通道分离的步骤:

添加一个Shader UI/DefaultETC1

unity cube 6个面_unity_38

选择完格式之后勾选分离Alpha通道

unity cube 6个面_unity_39

 

34,动态图集

通过消耗较小的图集打包的时间来规避一次性打出非常多的图片的图集在加载时的性能消耗,加载时只去加载这少部分的图集即可,主要用于部分场景需要用户去确定实际加载比较多的不确定性的小图时,无法确定哪些图片会被打入同一个图集中才能降低dc

算法:

unity cube 6个面_UI_40

应用场景实现

 

35,Unity的图集算法

旧版:

默认的矩形区域打包

紧密打包,注意使用紧密打包可能会使得某张Sprite携带到其他Sprite的渲染信息,此时需要勾选Image的UseSpriteMesh才能消除其它的Sprite渲染部分

unity cube 6个面_Mask_41

 

默认矩形区域打包时单张图片的紧密打包:

unity cube 6个面_Graphic_42

紧密打包时单张图片的矩形区域打包:

unity cube 6个面_Graphic_43

 

精灵的多边形模式:

可以设置它的包围框成一个指定边数的多边形

unity cube 6个面_unity cube 6个面_44

 

CustomOutline:

自定义当前SpriteMesh的范围,减少OverDraw

unity cube 6个面_unity_45

unity cube 6个面_UI_46

 

Unity2018之前没有这个属性,勾上之后会以当前这个Sprite的Mesh进行顶点和面片的绘制,可以减少OverDraw,但是相应的增加了顶点数和面片数

unity cube 6个面_Graphic_47

 

36,TexturePacker

unity cube 6个面_unity cube 6个面_48

unity cube 6个面_unity cube 6个面_49

 

和Unity自身的图集打包算法比较:

Texture算法更优,整体的Overdraw和顶点面片计算更优,最大的优势更灵活

Unity屏蔽了大多数复杂的实现细节,使用更方便

 

*37,Unity调起命令行

unity cube 6个面_Graphic_50

unity cube 6个面_Mask_51

 

38,减少不必要的OverDraw

游戏中经常会有一些“点击任意区域关闭Panel”的提示,这种情况下我们通常是加一张全屏的Image和Button组件,此时Image的Mesh仍然在绘制,那么可以编写一个自定义的组件,清除Mesh的渲染部分,那么就可以减少这部分的OverDraw和顶点,面片的绘制

unity cube 6个面_Mask_52

 

39,Texture的参数

Read/Write:

可读写,能不勾就不勾,一般用在获取像素信息或者写入像素信息,勾选之后会在内存中另外创建一份相同的副本

GenerateMipMaps:

在3D游戏中有近景和远景,勾选这个之后会根据位置信息生成不同分辨率的副本,虽然能在一定程度上节省图片加载内存,但是开辟了额外的空间,在2D游戏中都是关闭这个选项

unity cube 6个面_unity cube 6个面_53

 

*40,AssetPostProcessor

资源导入时可以进行的一些参数设置,主要用在导入时的统一化参数或格式设置:

unity cube 6个面_unity_54

 

在导入Texture时自动转化为Sprite格式:

unity cube 6个面_Mask_55

 

*41,处理图片的镜像案例

unity cube 6个面_unity_56

 

剔除无用顶点的主要逻辑:

unity cube 6个面_Mask_57

InnerUV和OuterUV的区别:

UV左下角(0,0),右上角(1,1)

如果是单张图,那么InnerUV和OuterUV没有区别,如果是应用于图集当中,那么InnerUV是图集当中小图UV的,OuterUV是图集当中小图相对于整张大图的UV的