基础知识介绍

相关组件

在介绍NGUI的面板绘制原理前,需要提前对一些NGUI的组件进行了解。

  • UIPanel: 管理他下面所有Widget,窗口的裁剪,以及指派DrawCall的创建与更新
  • UIWidget:一种矩形容器组件,是UILabel、UITexture、UISprite、和UI2DSprite的基类
  • UIGeometry:是UIWidget的几何数据,记录了顶点坐标、贴图的UVs和颜色等信息
  • Mesh:需要被绘制的元素都需要Mesh网格组件,其中包含顶点、UV、Color、切线、法线、三角面信息
  • MeshFilter:网格过滤器,会从资源中获取Mesh信息,并将其传递给网格渲染器进行渲染
  • MeshRenderer:网格渲染器,用于渲染网格。若MeshRenderer组件被移除,将不会渲染该网格

渲染层级的决定因素

a) Camera.Depth: 相机的Depth值越大,渲染的图像越靠前,优先级最高

b) render.sortingOrder: 修改UIPanel的sort order值,相当于改了其中DrawCalls的sortingOrder值,也就是改变了Renderer的sortingOrder值

c) Render Queue: Material和Shader都有该属性,它的值一般是从3000开始的,每新创建一个UIDrawcall,其对应material的render queue的值就会加1,因此UIDrawcall是靠render queue排序的

d) 顶点缓存序列的先后: 即UIGeometry中传递的vertex序列,UIWidget在遍历排序时,通过一定的排序算法,在列表中靠前位置的Widget的vertex会先传入UIGeometry缓存中,因此该vertex在传入UIDrawcall后会优先生成Mesh并渲染

UIPanel和UIWidget都有一个depth属性,共同决定着组件的最终深度值,而NGUI中主要就是根据这个深度值的顺序去进行渲染。一般情况下,UIPanel的depth权重远远高于UIWidget的depth权重。

DrawCall Batching技术

在Unity中,每次引擎准备数据并通知GPU的过程称为一次Drawcall。Unity的DrawCall Batching(合批处理)技术,目标是在一次DrawCall的过程中批量处理多个物体。只要物体的材质相同,GPU就可以按照相同的方式进行处理,因此可以将其合并为一个DrawCall进行处理。

UIPanel、UIWidget、UIGeometry和UIDrawcall的关系

在了解NGUI的面板绘制原理前,先用一张图列出各组件间的关系,

每个UIPanel只负责管理它下面所有的Widget,根据Widget的深度值进行排序,然后对有序的Widget列表中材质相同的相邻Widget进行DrawCall的合并。

UIPanel负责管理UIWidget和UIDrawcall的列表,控制UIWidget的插入、调整等操作。

当插入UIWidget时,根据一定规则将UIWidget插入到Widget列表的相应位置中,然后根据UIWidget的深度、材质和位置关系判断是复用DrawCall还是创建新的DrawCall。

UIDrawCall相当于对DrawCall进行了一层封装,它能完成一些比如创建和更新材质、从Geometry中获取缓存数据并添加网格相关的组件、对网格数据进行设置等工作,从而完成面板的渲染。

每一个UIWidget都对应一个UIGeometry,UIGeometry用于记录widget的顶点信息、UVs、颜色信息等。当UIWidget有变动时都会刷新Geometry中的数据。在UIPanel的LateUpdate生命周期回调中,会指派所有UIDrawcall去对相应Mesh网格进行数据的填充,从而完成绘制工作。

UIPanel的工作原理

为了深入了解NGUI中面板的绘制流程,我们必须明白UIPanel的工作原理。

在UIPanel中有两个比较重要的数据变量:

其中,widgets是一个有序的UIWidget的列表,通过搜索可知,唯一一处调用list.Add()list.Insert()地方是在UIPanel的UIPanel.AddWidget()方法中,而调用UIPanel.AddWidget()的地方有UIWidget.CreatePanel() 和UIWidget中depth属性的set方法。

drawCalls是UIPanel中持有的一个记录该Panel中所有DrawCall的列表,在有新的DrawCall产生时都会将其插入到drawCalls中。在添加新的DrawCall或更新DrawCall是都会通知Geometry刷新数据。在UIPanel的LateUpdate的最后都会为所有面板的DrawCall设置正确的renderQueue值,从而能根据该值进行正确顺序的渲染。

在介绍UIPanel的工作原理时,主要从两个重要的方法入手(UIPanel.AddWidget和UIPanel.LateUpdate)。

UIPanel.AddWidget

panel.AddWidget(w)方法会向widgets中插入Widget。

在向widgets中插入Widget时,会根据UIWidget.PanelCompareFunc()比较目标Widget,然后将当前Widget插入到符合条件的位置。

UIWidget.PanelCompareFunc()比较方法的规则如下:

① Widget的depth值小的在前,depth值大的在后

② 若depth相同,则优先有material的widget在前

③ 若depth相同且都有material,则material的instance id值小的在前

在将widget插入到widgets的合理位置后,会进行FindDrawCall(w),在这个过程中,会遍历整个UIPanel中的drawcalls,如果widget满足要求,则复用现有的DrawCall;若不满足要求,则需要创建一个新的DrawCall。

FindDrawCall复用已有drawcall的规则如下:

① depth的范围合理(Widget的depth值在目标DrawCall的dcStart和dcEnd值之间)

② Widget的material和texture值与目标DrawCall中的值相同

③ Widget可见

UIPanel.LateUpdate

UIPanel对于Widget的信息写入Geometry缓存、调用DrawCall创建Mesh等操作都是在UIPanel的LateUpdate的生命周期回调内完成的。

其整过程分为两个部分,即:①更新所有UIPanel及其各自部分的组件信息;②更新所有DrawCall并设置正确的绘制顺序。

首先会遍历整个Panel的list,对每一个Panel中的widgets,drawcalls进行更新。

在UIPanel的UpdateSelf中,会进行如下工作:

  • UpdateTransformMatrix: 更新world-to-local变换矩阵
  • UpdateLayers: 若Panel的layer改变,则更新Widget的layer值
  • UpdateWidgets: 更新Panel中的所有Widget

然后是其中比较重要的方法 FillAllDrawCalls()FillDrawCall(dc),如果需要重新构建,则调用前者清空drawCalls并重新创建drawCalls列表并更新Geomerty数据,否则调用后者更新指定DrawCall并更新与之相关的Widget的Geometry数据。

FillAllDrawCalls

FillAllDrawCalls()会重新创建所有的DrawCall,清空之前所有的DrawCall,按UIWidget.PanelCompareFunc()对所有Widget进行排序。遍历所有Widget,对比该Widget的material、texture和shader是否都与前一个Widget相同,若有不同,则创建一个新的DrawCall;若相同,则合并DrawCall(Draw Call Batching)。完成DrawCall创建后会将Widget中的verts、uvs、cols等信息写入其Geometry中,并调用UIDrawCall.UpdateGeometry创建Mesh、MeshFilter和MeshRender组件去渲染界面元素。

FillDrawCall

FillDrawCall(dc)对指定DrawCall进行更新。遍历所有Widget,若当前Widget所使用的DrawCall与指定DrawCall相同,则更新Widget的Geometry数据,最后同样调用UIDrawCall.UpdateGeometry渲染界面。

因此,LateUpdate回调的整体流程如下图所示:

在LateUpdate的最后,会遍历所有的Panel,并对每个Panel中的DrawCall进行更新并设置其renderQueue属性,在进行界面绘制的时候会根据DrawCall的renderQueue值进行有序的绘制。

减少DrawCall数量的建议

  1. 同一Panel下的贴图资源尽量打包在一个图集中。
  2. 如果同一panel中使用了多个图集,则应当尽量保证使用相同图集的元素在连续的深度范围内,使用不同图集的元素之间尽量不要有深度的穿插。