NGUI所见即所得之UIPanel_xiaodiao062的专栏-程序员宅基地

技术标签: NGUI  

之前在 NGUI所见即所得之UIWidget , UIGeometry & UIDrawCall 文中就这样用过这样的一个例子:

                UIGeometry好比为煮菜准备食材,UIDrawCall好比是煮菜的工具(锅,炉子等),UIPanel就是大厨了决定着什么时候该煮菜,UIWidget(UILabel,UISprite和UITexture)是这道菜怎么样的最终呈现。

         本来不打算继续写UIPanel的内容的,因为没有这么深刻的需求,后面自己根据FastGUI生成的UI发现DrawCall数很多:

       一个很简单的界面竟然用了9个DrawCall,相同的material没有进行DrawCall完全的合并,相对于以前一个Material一个DrawCall是不可接受的,所以这样就很必要去看下UIPanel都做了哪些事情,看下了NGUI的更新日志有这样的一句话:

       3.0.0:

       - NEW: Changed the way widgets get batched, properly fixing all remaining Z/depth issues.

       - NEW: Draw calls are now automatically split up as needed (no more sandwiching issues!)

       NGUI之前的版本关于组件的显示跟Z周,depth以及图集和UIPanel的关系一直都受到大家吐槽和诟病(尤其夹层问题),所以NGUI3.0.3就彻底解决这个问题:使用DrawCall切割,然后由depth完全决定组件显示的前后。

        也就是说,NGUI对DrawCall进行了分割处理,导致DrawCall数量“剧烈”增加,所以要解决DrawCall数量增加,就要UIPanel产生一个UIDrawCall的原理,然后减少UIDrawCall的生成或进行合并。

        从上图,可以发现NGUI还是对部分组件进行了UIDrawCall合并——多个UIWidget使用同一个UIDrawCall,所以要想做到同一个Material使用一个UIDrawCall理论上是完全可行的。

再说UIWidget,UIGeometry&UIDrawCall

        虽然已经有NGUI所见即所得之UIWidget , UIGeometry & UIDrawCall 一文,但是由于之前是在几乎忽略UIPanel的情况下理顺UIWiget,UIGeometry&UIDrawCall三者的关系的,所以文中的组织逻辑比较混乱,条理不强,加上本文也是建立者三者之上的,作为行为的结构的流畅性和完整性,所以还是在简要交代下。
       上图是UIWidget,UIGeometry&UIDrawCall的关系图,UIWidget用于UIDrawcall mDrawCall和UIGeometry mGeo两个成员变量,其中UIGeometry就是对UIWidget的顶点vertices,uvs和color进行存储和更新,UIDrawCall就是根据提供的数据(统一在UIPanel指派)进行渲染绘制。


       
UIGeometry完全由UIWidget维护,首先UILabel,UISprite,UITexture对UIWidget的OnFill进行重写——初始化mGeo的verts,uvs,cols的BetterList。然后UIWidget的UpdateGeometry函数对UIGeometry的ApplyTransform()和WriteToBuffer()调用进行更新。

         每一个UIWidget都有一个UIGeometry,但是并不都有一个UIDrawCall,而是要通过Batch合并达到减少DrawCall的数量,UIDrawCall是由UIPanel生成的。至于什么是DrawCall,因为没有3D引擎经验,只能从只言片语中拾获一点理解:

      

             “Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),       索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU       ——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。 在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。这一过程是逐个物体进行的,对       于每个物体,不只GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标。”

    

       NGUI被说的最多的优点就是:减少DrawCall数量。但现在为了解决sandwiching issues和Z/depth issues,对DrawCall进行split。

NGUI指派DrawCall的原理

  

       前面说到,UIDrawCall是由UIPanel生成指派的,哪些UIWiget共用(也就是Batch)一个DrawCall在UIPanel中决定的。UIDrawCall有一个静态变量:

C#代码 复制代码 收藏代码
  1. /// <summary> 
  2. /// All draw calls created by the panels. 
  3. /// </summary> 
  4. static public BetterList<UIDrawCall> list = new BetterList<UIDrawCall>(); 

      也就是说所有的UIDrawCall都会保存在list中,都说“大蛇要打七寸”,只要找到哪里有 list.add 的调用就知道生成增加了一个UIDrawCall,这样就找到GetDrawCall函数(也可以通过MonoBehaviour的调试功能打断点进行函数跟踪):

C#代码 复制代码 收藏代码
  1.     /// <summary> 
  2.     /// Get a draw call at the specified index position. 
  3.     /// </summary> 
  4.  
  5.     UIDrawCall GetDrawCall (int index, Material mat) 
  6.     { 
  7.         if (index < UIDrawCall.list.size) 
  8.         { 
  9.             UIDrawCall dc = UIDrawCall.list.buffer[index]; 
  10.  
  11.             // If the material and texture match, keep using the same draw call 
  12.             if (dc != null && dc.panel == this && dc.baseMaterial == mat && dc.mainTexture == mat.mainTexture) return dc; 
  13.  
  14.             // Otherwise we need to destroy all the draw calls that follow 
  15.             for (int i = UIDrawCall.list.size; i > index; ) 
  16.             { 
  17.                 UIDrawCall rem = UIDrawCall.list.buffer[--i]; 
  18.                 DestroyDrawCall(rem, i); 
  19.             } 
  20.         } 
  21. #if UNITY_EDITOR 
  22.         // If we're in the editor, create the game object with hide flags set right away 
  23.         GameObject go = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags("_UIDrawCall [" + mat.name + "]"
  24.             //HideFlags.DontSave | HideFlags.NotEditable); 
  25.             HideFlags.HideAndDontSave); 
  26. #else 
  27.         GameObject go = new GameObject("_UIDrawCall [" + mat.name + "]"); 
  28.         DontDestroyOnLoad(go); 
  29. #endif 
  30.         go.layer = cachedGameObject.layer; 
  31.          
  32.         // Create the draw call 
  33.         UIDrawCall drawCall = go.AddComponent<UIDrawCall>(); 
  34.         drawCall.baseMaterial = mat; 
  35.         drawCall.renderQueue = UIDrawCall.list.size; 
  36.         drawCall.panel = this
  37.         //Debug.Log("Added DC " + mat.name + " as " + UIDrawCall.list.size); 
  38.         UIDrawCall.list.Add(drawCall); 
  39.         return drawCall; 
  40.     } 

       进一步找到Fill()的调用:

C#代码 复制代码 收藏代码
  1. /// <summary> 
  2. /// Fill the geometry fully, processing all widgets and re-creating all draw calls. 
  3. /// </summary> 
  4.  
  5. static void Fill () 
  6.     for (int i = UIDrawCall.list.size; i > 0; ) 
  7.         DestroyDrawCall(UIDrawCall.list[--i], i); 
  8.  
  9.     int index = 0; 
  10.     UIPanel pan = null
  11.     Material mat = null
  12.     UIDrawCall dc = null
  13.  
  14.     for (int i = 0; i < UIWidget.list.size; ) 
  15.     { 
  16.         UIWidget w = UIWidget.list[i]; 
  17.  
  18.         if (w == null
  19.         { 
  20.             UIWidget.list.RemoveAt(i); 
  21.             continue
  22.         } 
  23.  
  24.         if (w.isVisible && w.hasVertices) 
  25.         { 
  26.             if (pan != w.panel || mat != w.material)    //a) 
  27.             { 
  28.                 if (pan != null && mat != null && mVerts.size != 0) 
  29.                 { 
  30.                     pan.SubmitDrawCall(dc); 
  31.                     dc = null
  32.                 } 
  33.  
  34.                 pan = w.panel; 
  35.                 mat = w.material; 
  36.             } 
  37.  
  38.             if (pan != null && mat != null)   //b) 
  39.             { 
  40.                 if (dc == null) dc = pan.GetDrawCall(index++, mat); 
  41.                 w.drawCall = dc; 
  42.                 if (pan.generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans); 
  43.                 else w.WriteToBuffers(mVerts, mUvs, mCols, null, null); 
  44.             } 
  45.         } 
  46.         else w.drawCall = null
  47.         ++i; 
  48.     } 
  49.  
  50.     if (mVerts.size != 0) 
  51.         pan.SubmitDrawCall(dc); 

整理Fill函数的原理如下r:

       (1) 获取UIWidget的队列UIWidget.list(已经根据depth排好序),声明一个UIPanel pan,Material mat和UIDrawCall dc,pan,mat和dc都是保存上一次循环的UIPanel,Material和UIDrawCall。

       (2) 遍历UIWidget.list,循环体中对 当前UIWiget w的panel和material是否和当前pan,mat是否相同 进行判断,分为两种情况:

                  a)如果有一种不相同,调用SubmitDrawCall函数,SubmitDrawCall函数其实就是使用pan的mVerts, mUvs, mCols数据,调用UIDrawCall的set函数对Mesh,MeshRender,MeshFilter等进行“设置组装”。

                  b)如果相同,通过调用GetDrawCall获取当前pan和mat的DrawCall,然后将UIWidget w的UIGeometry数据放入mVerts, mUvs, mCols(通过调用函数w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans)) 

     小结:UIPanel的mVerts,mUVs,mCols只是要将要传给UIDrawCall数据的一个“积蓄”过渡的一个概念,也就是说,Fill函数式这么操作的:先将UIWidget w的中UIGeometry的数据缓存在UIPanel的mVerts,mUVs,mCols,只有当不能再pan或mat与当前的w.panel或w.material不同时就不能再缓存了,然后通过SubmitDrawCall,生成UIDrawCall的工作才完成,然后再重新 new 一个新的UIDrawCall继续缓存数据。

UIPanel完整工作流程——LateUpdate

     前面介绍UIDrawCall的产生过程,当然这是UIPanel最重要的工作之一,在对UIDrawCall进行更新是要对UIPanel的其他信息(transform,layer,widget)等进行更新:

C#代码 复制代码 收藏代码
  1. /// <summary> 
  2. /// Main update function 
  3. /// </summary> 
  4.  
  5. void LateUpdate () 
  6.     // Only the very first panel should be doing the update logic 
  7.     if (list[0] != this) return
  8.  
  9.     // Update all panels 
  10.     for (int i = 0; i < list.size; ++i) 
  11.     { 
  12.         UIPanel panel = list[i]; 
  13.         panel.mUpdateTime = RealTime.time; 
  14.         panel.UpdateTransformMatrix(); 
  15.         panel.UpdateLayers(); 
  16.         panel.UpdateWidgets(); 
  17.     } 
  18.     // Fill the draw calls for all of the changed materials 
  19.     if (mFullRebuild) 
  20.     { 
  21.         UIWidget.list.Sort(UIWidget.CompareFunc); 
  22.         Fill(); 
  23.     } 
  24.     else 
  25.     { 
  26.         for (int i = 0; i < UIDrawCall.list.size; ) 
  27.         { 
  28.             UIDrawCall dc = UIDrawCall.list[i]; 
  29.  
  30.             if (dc.isDirty) 
  31.             { 
  32.                 if (!Fill(dc)) 
  33.                 { 
  34.                     DestroyDrawCall(dc, i); 
  35.                     continue
  36.                 } 
  37.             } 
  38.             ++i; 
  39.         } 
  40.     } 
  41.  
  42.     // Update the clipping rects 
  43.     for (int i = 0; i < list.size; ++i) 
  44.     { 
  45.         UIPanel panel = list[i]; 
  46.         panel.UpdateDrawcalls(); 
  47.     } 
  48.     mFullRebuild = false

        就不进行文字描述了,贴一张自己的画的LateUpdate()函数调用栈图(不光文笔不好,画图也不行,硬伤呀,就这样也是琢磨很久画的):

DrawCall数量优化

         言归正传,本文的话题就是对于NGUI3.0.4的版本(目前最新版)如何减少DrawCall, 先回到文中的第一幅图,发现两个以New atlas图集为material的DrawCall夹着一个以font为字体集的DrawCall间隔,然后使用MonoBehaviour的断点调试功能进行跟踪得到UIWidget.list队列:



        发现一个规律:使用相同material的连续UIWidget(UILabel,UISprite)共用一个UIDrawCall。这样就给了一个解决策略:对UIWidget.list进行排序,使得使用相同的material的UIWidget在UIWidget.list相连,而UIWidget.list是根据UIWidget的depth进行排序的。所以可以有如下两种方法:

        1)修改UIWidget(UILabel,UISprite)的depth,限定好UIWidget.list的排序

        2)重写UIWidget的CompareFunc方法。

C#代码 复制代码 收藏代码
  1. /// <summary> 
  2. /// Static widget comparison function used for depth sorting. 
  3. /// </summary> 
  4.  
  5. static public int CompareFunc (UIWidget left, UIWidget right) 
  6.     int val = UIPanel.CompareFunc(left.mPanel, right.mPanel); 
  7.  
  8.     if (val == 0) 
  9.     { 
  10.         if (left.mDepth < right.mDepth) return -1; 
  11.         if (left.mDepth > right.mDepth) return 1; 
  12.  
  13.         Material leftMat = left.material; 
  14.         Material rightMat = right.material; 
  15.  
  16.         if (leftMat == rightMat) return 0; 
  17.         if (leftMat != null) return -1; 
  18.         if (rightMat != null) return 1; 
  19.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1; 
  20.     } 
  21.     return val; 

         下面对原来对属于第三个DrawCall的两个UILabel增大他们的depth,发现DrawCall立马减少一个了,说明这个方法是可行的:

   

        同理,重写UIWidget的CompareFunc也是可以的,按照Material的name优先排序,只有当material一样是才考虑depth进行排序:

C#代码 复制代码 收藏代码
  1. /// <summary> 
  2. /// Static widget comparison function used for depth sorting. 
  3. /// </summary> 
  4.  
  5. static public int CompareFunc (UIWidget left, UIWidget right) 
  6.     int val = UIPanel.CompareFunc(left.mPanel, right.mPanel); 
  7.  
  8.     if (val == 0) 
  9.     { 
  10.         //原理排序的方法 
  11.         /*if (left.mDepth < right.mDepth) return -1;
  12.         if (left.mDepth > right.mDepth) return 1;
  13.         Material leftMat = left.material;
  14.         Material rightMat = right.material;
  15.         if (leftMat == rightMat) return 0;
  16.         if (leftMat != null) return -1;
  17.         if (rightMat != null) return 1;
  18.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;*/ 
  19.          
  20.  
  21.         Material leftMat = left.material; 
  22.         Material rightMat = right.material; 
  23.  
  24.         if (leftMat == rightMat)  
  25.         { 
  26.             if (left.mDepth < right.mDepth) return -1; 
  27.             else if (left.mDepth > right.mDepth) return 1; 
  28.             else return 0; 
  29.         } 
  30.         if(leftMat !=null & rightMat != null
  31.             return string.Compare(leftMat.name,rightMat.name); 
  32.         if (leftMat != null) return -1; 
  33.         if (rightMat != null) return 1; 
  34.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1; 
  35.          
  36.     } 
  37.     return val; 

       最终的DrawCall数量一定是等于使用的Material的数量:

还是夹层问题(sandwiching issues!)

       现在我们完全可以实现一个Material一个DrawCall,但是这样还是没有解决夹层的难题,NGUI给我们解决方法就是多一个DrawCall,这个其实跟3.0之前的版本多用一个UIPanel或者UIAtlas是一样的道理。这样还是感觉没有从本质上解决这个问题,只是换了一种方式权衡了一下。

       记得NGUI3.0之前的版本还是有Z轴的概念的,现在Z轴完全是形同虚设,但是3D引擎的图形一定是跟Z轴是密切的关系的,而最终的图形显示的位置关系是由Mesh的顶点决定的,所以可以考虑Z轴来解决夹层问题:DrawCall控制的渲染队列的次序renderQueue,Mesh控制的是实际绘制的“地理位置”,如下图所示,A和C使用相同的图集有相同的material,B单独使用一个图集,可以通过material来排序或者定制好depth,让A和C使用一个DrawCall,但是C的Mesh(参考transform的Z轴)会在B的“前面”,这样就应该可以实现夹层的效果了。

       做了下测试,修改Mesh的Z轴没有什么变化,然后早上向同事请教了,因为Material使用的Shader使用了透明,这样就不能做深度测试,也就是Mesh的“深度”是不影响的,这样最终的显示就跟Shader的renderQueue有关了,即renderQueue越大,显示的越靠前面(重叠的图层,renderQueue越大,越靠前)。当然现在只有增加一个DrawCall(如多使用一个UIPanel或用另外一个Material)来做到Material的夹层效果。
       

小结:

        NGUI更新的很快,之前一直也没有仔细研究,最近开始慢慢看了些,也写了些博客,主要有3点收获:1)NGUI的渲染机制,2)NGUI相关“组件”(Font,Atlas,UIWidget等)实现方法,3)NGUI的设计模式。当然D.S.Qiu觉得NGUI作为一个大的系统一定会有冗余和诟病,使用了很多“缓存”的思想,很多细节都没有处理好,所以我们都可以再努力完善,争取做“站在巨人的肩膀上”的那个人。

        发现没有3D引擎以及图形渲染的基础,做点事情还是很蹩脚的,GPU的处理输出,显存的大小,CPU与GPU的交互都是要考虑的,有空的时候还是要找到这方面的书来恶补下……

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

        转载请在文首注明出处:http://dsqiu.iteye.com/blog/1973651

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xiaodiao062/article/details/32327415

智能推荐

java map传入参数_JAVA中map中参数的添加修改_慕如璟的博客-程序员宅基地

Map以按键/数值对的形式存储数据,和数组非常相似,在数组中存在的索引,它们本身也是对象。Map的接口Map---实现MapMap.Entry--Map的内部类,描述Map中的按键/数值对。SortedMap—扩展Map,使按键保持升序排列关于怎么使用,一般是选择Map的子类,而不直接用Map类。下面以HashMap为例。public static void m...

ASP.NET Core MVC 2.x 全面教程 笔记_银龙软件-程序员宅基地_asp.net core mvc教程

ASP.NET Core MVC 01. 创建项目 +项目结构和配置简介ASP.NET Core MVC 02. Web Host 的默认配置ASP.NET Core MVC 03. 服务注册和管道服务注册 public class Startup { // This method gets called by the ru...

linux打开python的ide_linux中使用Python IDE pycharm教程_Rabenda的博客-程序员宅基地

今天使用vim编辑Python 并在linux中终端调试的时候,发现每次不是自己想要输出结果的时候,就要用vim编辑代码,再重新回到终端,比较浪费时间。搜索发现pycharm这一个Python ide工具,之前没用过,折腾了一下午才知道怎么使用终端并调试运行自己的程序。网上的教程都不太全,下面给出自己的步骤。1、安装pycharm2.2安装pycharm的JDK环境Pycharm需要JDK环境解析...

Meshlab快捷键与菜单功能翻译_行秋的博客-程序员宅基地_meshlab快捷键

【Meshlab快捷键】ctrl+N 新建项目ctrl+O 打开项目ctrl+S 存储项目ctrl+I 导入网格ctrl+E 导出网格alt+R 重读ctrl+shift+R 重读所有ctrl+Q 退出esc 不编辑ctrl+P 应用过滤器ctrl+K 背面剔除ctrl+D 双面灯光ctrl+Y 特殊灯光alt+return 全屏ctrl+L 显示图层对话框shift+R 显示当前光栅模式ctrl+H 重置轨迹球ctrl+J 从光栅相机查看ctrl+C 复制截图ct

01-Android开发基于eclipse的开发环境搭建_weixin_33804990的博客-程序员宅基地

为什么80%的码农都做不了架构师?&gt;&gt;&gt; ...

计算机网络拓扑结构可如何分类,计算机网络拓扑结构的分类_刘恺威的博客-程序员宅基地

《计算机网络拓扑结构的分类》由会员分享,可在线阅读,更多相关《计算机网络拓扑结构的分类(8页珍藏版)》请在人人文库网上搜索。1、计算机网络拓扑结构的分类,1,2,学习目标,知识与能力目标: 1、理解掌握星型网络拓扑结构及其特点。 2、了解总线型、环型、树型和网状型拓扑结构及其特点。 过程与方法目标: 让学生在学习的过程中实现学会、会学、乐学的统一。 情感态度与价值观目标: 通过情景模拟、视频教学、...

随便推点

linux下层次遍历目录,linux shell 命令怎么遍历目录_三杉的博客-程序员宅基地

shell 遍历目录中所有文件 改名分享一个shell脚本,解决以下问题:以日期命名建立了几百个目录,每个目录#。/bin/bashbase_dir=$(dirname $0)for dir in $(ls $base_dir); do current_dir="$base_dir/$dir" if [ 。 -d $current_dir ]; then continue fi for old_f...

jstack 脚本 自动日志_jstack dump日志文件详解_雪鱼子的博客-程序员宅基地

jstack Dump 日志文件中的线程状态dump 文件里,值得关注的线程状态有:死锁,Deadlock(重点关注)执行中,Runnable等待资源,Waiting on condition(重点关注)等待获取监视器,Waiting on monitor entry(重点关注)暂停,Suspended对象等待中,Object.wait() 或 TIMED_WAITING阻塞,Blocked(重点...

虚拟机里切换linux图形界面,虚拟机-linux系统中图形界面和命令行界面切换_Fayyy Li的博客-程序员宅基地

linux系统中图形化界面和命令行界面之间的切换可以分为两种,临时性切换和永久性切换。临时性切换即切换后只对本次生效,系统重启后界面还是默认界面。永久性切换即切换后系统开机后永远处于的界面。临时性切换方法图形界面切换命令行界面:打开控制终端,输入init 3,然后回车如果提示没有权限,则需要先切换到root用户,再执行init 3进行切换命令行界面切换图形界面:输入init 5,然后回车永久性切换...

后端日期类属性date 不接受string类型日期,都是没找到解决的方法,所有前端传回的string字符串都一一转化为java定义的类型..._weixin_34009794的博客-程序员宅基地

1、比如日期 我们可以是yyyy-MM-dd 亦可以是yyyy-MM-dd HH:mm:ss方法1在java代码中需要的字段上加上注解 写上日期类型,不过这样很麻烦,每个人写了日期类型的接收前端的参数时都要自己注入,一不小心就有前端来找你接口500了,所以这个方法一有点自私 我不用。方法2 一起xml写好到处都可以用 &lt;mvc:annotation-driven va...

oracle10g rac搭建dg,RAC上配置DG_李嘶嘶的博客-程序员宅基地

这段时间闲来无事,打算把这段时间在RAC的一些应用配置整理一下。包括OGG,dataguard这些 今天先发一个 日后会继续更新的。关于这篇文章也就是搭个实验环境,真正的生产库还需要根据实际环境修改参数。ORACLE RAC + DATA GUARD配置一.配置信息RAChostnameRAC1RAC2Public ip172.16.30.11172.16.30.12Private ip192.1...

Arduino 光敏电阻调节呼吸灯_比特字节-只为技术-程序员宅基地

今天测试PWM做呼吸灯,以及用光敏电阻做达文西呼吸灯,并输出串口光敏AD值,可作为智能家居中,卧室慢慢亮起的地灯,或根据室内光线调节光亮等效果。

推荐文章

热门文章

相关标签