NGUI所见即所得之UIWidget , UIGeometry & UIDrawCall

作者:雨辰 发布于:2015-3-23 0:07 Monday 分类:Unity3D

 UIWidget是所有UI组件的抽象基类,作为基类当然定义了必须的成员变量和函数,接触过MFC或其他UI组件开发,想必都知道有一堆参数设置,尤其是Visual Studio的可视化界面,简直太丰富了,UIWidget要当UI组件的爹就必须得具备这些,下面就一一介绍:

 

Pivot

       Pivot,这个枚举,其实定义了GameObject中心坐标在整个组件的位置,这个跟UIStretch很类似,只不过UIStretch说的是组件相对于屏幕的位置。

C#代码  收藏代码
  1. public enum Pivot  
  2.     {  
  3.         TopLeft,  
  4.         Top,  
  5.         TopRight,  
  6.         Left,  
  7.         Center,  
  8.         Right,  
  9.         BottomLeft,  
  10.         Bottom,  
  11.         BottomRight,  
  12.     }  

        Pivot可以提供开发者更多的定位模式,可以方便实现组件对齐,如对个UILabel组件的文本居中对齐。当然这样,在计算组件的四个角的顶点坐标(localCorners)就得考虑Pivot。

 

localCorners,worldCorners & innerWorldCorners

        这三个变量都是四个角的顶点坐标,只是localCorners是计算局部的(相对gameObject的中心而言)坐标,worldCorners只是将localCorners作为世界坐标空间的坐标,innerWorlCorners则考虑了边框Border。

C#代码  收藏代码
  1. public virtual Vector3[] localCorners  
  2. {  
  3.     get  
  4.     {  
  5.         Vector2 offset = pivotOffset;  
  6.   
  7.         float x0 = -offset.x * mWidth;  
  8.         float y0 = -offset.y * mHeight;  
  9.         float x1 = x0 + mWidth;  
  10.         float y1 = y0 + mHeight;  
  11.   
  12.         mCorners[0] = new Vector3(x0, y0, 0f);  
  13.         mCorners[1] = new Vector3(x0, y1, 0f);  
  14.         mCorners[2] = new Vector3(x1, y1, 0f);  
  15.         mCorners[3] = new Vector3(x1, y0, 0f);  
  16.   
  17.         return mCorners;  
  18.     }  
  19. }  

 上面代码中的pivotOffset的计算就考虑了Pivot:

C#代码  收藏代码
  1. static public Vector2 GetPivotOffset (UIWidget.Pivot pv)  
  2.     {  
  3.         Vector2 v = Vector2.zero;  
  4.   
  5.         if (pv == UIWidget.Pivot.Top || pv == UIWidget.Pivot.Center || pv == UIWidget.Pivot.Bottom) v.x = 0.5f;  
  6.         else if (pv == UIWidget.Pivot.TopRight || pv == UIWidget.Pivot.Right || pv == UIWidget.Pivot.BottomRight) v.x = 1f;  
  7.         else v.x = 0f;  
  8.   
  9.         if (pv == UIWidget.Pivot.Left || pv == UIWidget.Pivot.Center || pv == UIWidget.Pivot.Right) v.y = 0.5f;  
  10.         else if (pv == UIWidget.Pivot.TopLeft || pv == UIWidget.Pivot.Top || pv == UIWidget.Pivot.TopRight) v.y = 1f;  
  11.         else v.y = 0f;  
  12.   
  13.         return v;  
  14.     }  

         发现pivotOffset是一个由0,0.5,1组成的二维向量,所以前面计算localCorners的原理就可想而知了。

         当然还有诸如width(minWidth),height(minHeight),depth(raycastDepth),alpha(finalAlpha),看了代码就自然一目了然了。rayCastDepth和finalAlpha都考虑了UIPanel的因素,所以有时表层看着没问题,可能就要去看下底层的实现。

 

两大基石:UIDrawCall和UIGeometry

       根据我有限的3D知识(其实没有学过),隐约觉得3D呈现出来的东西都是由顶点(vertice)构成的Mesh通过纹理贴图构成的材质渲染出来的。

        但是NGUI所有组件都没有看到Mesh Render,只有transform信息和脚本,查看UIWidget:

C#代码  收藏代码
  1.         /// <summary>  
  2. /// Internal usage -- draw call that's drawing the widget.  
  3. /// </summary>  
  4.   
  5. public UIDrawCall drawCall { getset; }  
  6.   
  7. // Widget's generated geometry  
  8. UIGeometry mGeom = new UIGeometry();  

 这就是UIDrawCall和UIGeometry神一般的存在。

 

UIGeometry

         UIGeometry其实是UI组件的数据仓库,存储UI组件的Vertices,UVs和Colors,以及相对UIPanel的顶点Vertices,法向量和切线:

C#代码  收藏代码
  1. /// <summary>  
  2. /// Widget's vertices (before they get transformed).  
  3. /// </summary>  
  4.   
  5. public BetterList<Vector3> verts = new BetterList<Vector3>();  
  6.   
  7. /// <summary>  
  8. /// Widget's texture coordinates for the geometry's vertices.  
  9. /// </summary>  
  10.   
  11. public BetterList<Vector2> uvs = new BetterList<Vector2>();  
  12.   
  13. /// <summary>  
  14. /// Array of colors for the geometry's vertices.  
  15. /// </summary>  
  16.   
  17. public BetterList<Color32> cols = new BetterList<Color32>();  
  18.   
  19. // Relative-to-panel vertices, normal, and tangent  
  20. BetterList<Vector3> mRtpVerts = new BetterList<Vector3>();  
  21. Vector3 mRtpNormal;  
  22. Vector4 mRtpTan;  

 UIGeometry为此提供了三个函数,也可以说是为渲染绘制做的三个步骤:

Step 1:   Clear()    情况存储的信息

Step 2:   ApplyTransform()    将verts转化为相对UIPanel的顶点坐标mRtpVerts

Step 3:   WriteToBuffers()     将数据仓库缓存的数据添加到UIPanel的缓存数据队列中去。

 

          UIWidget分别用UpdateGeometry和WriteToBuffers对UIGeometry的ApplyTransform和WriteToBuffers进行封装调用,ApplyTransform是根据UIPanel的坐标调整Vertices的坐标,WriteToBuffers将UIGeometry的Vertices,UVs和Colors添加进UIPanel的Vertices,UVs和Colors的BetterList中。

          这里还有一个细节就是:UIGeometry中的Vertices,UVs和Colors的BetterList的buffer什么时候得到,因为这些都是UIWidget或其子类的信息,所以在UIWidget的子类UILabel,UISprite和UITexture中OnFill函数生成UIGeometry的BetterList的buffer。

 

 

 

UIDrawCall

       如果是UIGeometry为了渲染绘制准备数据,那么UIDrawCall其实是定义了渲染绘制需要的基本组件。这里拿煮菜做个比喻帮助理解:UIGeometry好比为煮菜准备食材,UIDrawCall好比是煮菜的工具(锅,炉子等),UIPanel就是大厨了决定着什么时候该煮菜,UIWidget(UILabel,UISprite和UITexture)是这道菜怎么样的最终呈现。会不会很好理解呢?

          下面就直接看下Set函数:

C#代码  收藏代码
  1. if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();  
  2. if (mRen == null) mRen = gameObject.GetComponent<MeshRenderer>();  

         在Set函数中就给gameObjdect添加了MeshFilter和MeshRenderer组件,当然还做了其他辅助的工作。

         此外,在UpdateMaterials惊奇的发现了UIPanel clipView的AlphaClip和SoftClip的实现,其实就是更换了Material的Shader,这也体现了Shader强大,不得不学呀,DSQiu在后面也会对Unity的Shader编程做一个整理。

 

回放细节,回笼锻造

          看了上面就会觉得写得的确是所见即所得——D.S.Qiu好像都没有看到什么要害,都说学武功就要学习上次心法,一点皮毛总是会惹来耻笑。

          还是回到UIWidget这个脚本中的两个函数:WriteToBuffers,OnFill,UpdateGeometry。

          WriteToBuffers和OnFill这两个函数都是将Vertices,UVs和Colors等add进参数的List中去,查看WriteToBuffers的调用出发现其参数是UIPanel的Vertices,UVs和Colors,而OnFill的参数是UIGeometry的Vertices,UVs和Colors。

          WriteToBuffers只是对UIGeometry的封装调用,也就是说将UIGeometry的Vertices,UVs和Colors等信息add进UIPanel的对应List中。

          在看UIGeometry的脚本,一直有一个疑问:UIGeometry的Vertices,UVs和Colors的List没有看到add方法的执行,只有在WriteToBuffers被add。直到看到了OnFill才恍然大悟,虽然UIWidget的OnFill是虚函数,没有具体实现,看了下UISprite重写的OnFill函数,就是把Vertices,UVs和Colors 添加到UIGeometry的Vertices,UVs和Colors中。

          转了一大圈,发现最后所有UI组件的Vertices,UVs和Colors都汇集到UIPanel的Vertices,UVs和Colors去了,然后UIPanel指定给UIDrawCall渲染就行了,具体细节还要等攻克UIPanel就明白了。

 

 

其他细节

       到这里差不多把本文的内容都掏干净了,剩下的就是看下UIWidget的一些实现细节,MakePixelPerfect():对gameObject的localPosition和locaScale进行微调和纠正。

SetDirty():调用UIPanel的SetDirty()对组件的变更进行重建(rebuilt)。

CreatPanel():这个函数可以得知,所有UI组件一定是放在UIPanel的子节点上的,从前面的分析也可以知道:因为是UIPanel对统一对组件进行渲染绘制的,所以如果没有UIPanel有点“皮之不存,毛将附焉”的意思。

这也越发激发D.S.Qiu 去啃UIPanel这个主心骨。

 

小结:

       总算对UIWidget,UIGeomerty 和UIDrawCall有了一个清晰的认识,这三个组件可以说是NGUI的背后无名英雄(平时使用都不会接触者三个脚本),功能很强大但很简单,真的很佩服NGUI的设计思路。

       但我也有点小微词:UIWidget都被定义成长方形的,要是能提供自定义UI的接口(如我想画一个圆形UI),那将会更强大;底层缓存了很多数据,这个是内存和开发者的代码逻辑也是要考虑到的。

       最后附上①画的NGUI框架图:


       如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

之前项目中用的NGUI的版本是3.0.7 f3,开始的时候感觉没有什么问题,直达最近项目UI的完成度比较高时,就突然出现掉帧很严重的现象,即使只有一个UI打开(其他都是active = false的情况下),打开profier,发现UIPanel LateUpdate 竟然占了CPU使用率的50%左右,这太恐怖了,虽然之前看到过有吐槽NGUI的机制的,但是我觉得为了保证通用牺牲一些性能还是在所难免的,但是没想到这个版本竟然这么废。

        之前虽然研究过NGUI的UIWidget, UIDrawCall,UIGeometry和 UIPanel等基础脚本(NGUI所见即所得之UIWidget , UIGeometry & UIDrawCallNGUI所见即所得之UIPanel),也大概清楚了NGUI的绘制原理。但对具体的逻辑还是不够清楚,有点凤毛麟角。为了更好的改进NGUI的性能以及更加规范使用NGUI,只有把NGUI的底层吃透。

        由于在之前的文章介绍了UIGeometry,UIDrawCall和UIWidget之间的关系,以及UIPanel的管理机制,所以本文主要剖析底层的原理,主要要弄清楚一下问题:

 

               1. transform ,大小(size)的变化的底层绘制影响

               2.颜色(包括透明度)变化的底层绘制影响

               3.enable 和 disable 状态变化底层的处理

               4.UIDrawCall 和 UIPanel 机制的细节

       

        未免读者理不顺,先简单说下UIGeometry,UIDrawCall和UIWidget的关系:UIWidget是UI的基础组件(UILabel,UISprite)的基类,含有组件的基本信息(width,Height,color等),UIGeometry是UIWidget的几何数据,记录了顶点坐标,贴图的UVs和颜色等信息,UIDrawCall是将多个UIWidget的UIGeometry组合起来一起绘制,具体的UIWidget如果共用一个UIDrawCall由UIPanel控制,要想了解更多可以点击上面的链接的文章查看。

        虽然从人的求知欲角度,我们的疑问是按照上面 1-4 排列的,但是下面却是从 4开始介绍,只要把4理解透了3,2,1就自然迎刃而解了。

UIDrawCall

        UIGeometry相对简单,这里就不再浪费篇幅介绍了,UIDrawCall是绘制的基础组件,还是有必要仔细介绍下。

1.成员变量

        仅对几个比较重要又搞不明白的变量进行解析:

        a)List<UIDrawCall> mActiveList 和 mInactiveList : 为什么会有两个List,mAcitveList 保持当前激活的UIDrawCall, mInactiveList主要是用于回收UIDrawCall.Destroy()的UIDrawCall,以达到循环利用避免内存的反复申请和释放,减少GC的次数。这个机制前面介绍的 vp_Timer采用这个策略。

        b)Material mMaterial 和 mDynamicMat:不是讲究节约内存么,怎么会有两个Material,mMaterial就是我们图集的材质Material,mDynamicMat是实际采用的Material,因为UIPanel 的 Clipping有 AlphaClipp 和 SoftClip 这两个是要通过切换Shader来实现的,所以需要对应动态创建一个Material,这个就是mDynamicMat的存在。

        c)bool mRebuildMat 和 isDirty:这两者表示UIDrawCall所处的状态,当改变UIDrawCall的 Material 和 Shader ,mRebuildMat就变为 true,就会引起 RebuildMaterial()的调用。isDirty若为 true ,表示UIDrawCall要进行重写“填充”,调用Set函数。

C#代码

 

 

2.几个重要的函数

        a)CreateMaterial, RebuildMaterial 和 UpdateMaterial,这是三个后面包含前面,总之就是完成材质的创建或更新。

        b)Set (BetterList<Vector3> verts,BetterList<Vector3> norms,BetterList<Vector4> tans,BetterList<Vector2> uvs,BetterList<Color32> cols),根据verts,norms,tans,uvs,cols重新构建Mesh,MeshRender。

 

c)OnEnable,Ondisable 和 OnDestroy:销毁了mDynamicMat,可以看出Material比Mesh更简单,不用太考虑内存问题,然后OnDestroy()没有发现调用。

 d)Create , Clear 和 Destroy:Create 先从mInactiveList中取出一个,在附上属性达到重复利用,Destroy是将没用的UIDrawCall从mActiveList移到mInactiveList中:

UIPanel

       之前就介绍过UIPanel,也画了UIPanel主要函数的调用栈(点击查看),这里也简单罗列下LateUpdate的函数调用:

 LateUpdate

      UpdateSelf

                UpdateTransformMatrix : 调整 worldToLocal 矩阵用于调整其管理的UIWidget的transform,并进一步调整顶点信息,还调整clipOffset的变量

                UpdateLayers : 更新LayerMask

                UpdateWidgets : 调整UIWidget

                            UIWidget.UpdateGeometry : 调整UIWidget的几何(顶点等)信息

                                              OnFill(geometry.verts, geometry.uvs, geometry.cols): 如果颜色(透明度)和大小等改变就重新填充顶点信息

                                              geometry.ApplyTransform : transform发生改变,调整UIGeometry中顶点的位置(矩阵计算)

                FillAllDrawCalls  or FillDrawCall : 重新构建所有UIDrawCall (当UIWdiget的depth发生变化),否则只调整有UIWidget的UIDrawCall

      UpdateDrawCalls : 调整UIPanel管理的UIDrawCall 的 transform 和 clip 等属性

      越来越觉得NGUI的代码组件结构越来越清晰,虽然篇幅很长(有1600多行)但理解还是可以很简单的。

 

UIWidget

       UIWidget有一个变量 mChange 和一个函数 MarkAsChange() 很重要,这两个标记UIWidget是否变化需要进行调整的状态。

                1.当 Anchor , Pivot , Alpha 以及 UILabel 和 UISprite 的一些状态的改变 mChange = true ,即会调整Geometry信息

                2.MarkAsChange 会执行 drawCall.isDirty = true; 这样就会导致其所属的 UIDrawCall 需要重写构建 

 

针对前面 1-3 的疑问进行如下总结:

      UIWidget(UILabel , UISprite)的任何变化(transform , drawSize , width , heigth , color , pivot ,anchor 等)变化都会引起绘制该UIWidget进行重新构建——对Mesh的顶点进行刷新,尤其是depth的变化会使得所有UIDrawCall 进行重写调整,这是非常耗性能的。

       

总结:

       NGUI的好处就是:合并Mesh和图集节省DrawCall,由于影响Mesh的因素太多了,所以会“牵一发而动全身”,NGUI采取的一个通用的策略,没有对不同的情况做不同的处理,都是采用某个UIDrawCall全部刷新甚至是全部UIDrawCall的刷新,这也是大家吐槽的“重中之重”。

       D.S.Qiu认为针对不用的情况还是会有不少优化的,比如改变alpha值,可以不需要重新调整顶点verts,而只需要单独调整cols的alpha通道,改变depth也不需要全部调整UIDrawCall,这样明显是没有做到严格的管理的。

       对此,D.S.Qiu提出2点使用NGUI制作UI的建议:

                1)尽量是UIWidget静动分离,即静止的尽量合成单独一个UIPanel,会变化的就放在另外一个UIPanel

                2)尽量控制UIPanel和UIDrawCall的数量,充分利用图集的空间,对“夹层”的情况可以通过图集的调整,使得UIDrawCall变得更少。


转载请注明出处:http://dsqiu.iteye.com/blog/1965340

标签: Unity3D-优化性能 Unity3D-Editer unity

发表评论:

雨辰 joyimp|@2011-2018 京ICP备16030765号