NGUI的面板绘制原理
基础知识介绍
相关组件
在介绍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数量的建议
- 同一Panel下的贴图资源尽量打包在一个图集中。
- 如果同一panel中使用了多个图集,则应当尽量保证使用相同图集的元素在连续的深度范围内,使用不同图集的元素之间尽量不要有深度的穿插。
- 本文链接:https://jygod.github.io/2019/06/23/NGUI的面板绘制原理/
- 版权声明:复制或引用请注明出处
分享