zoukankan      html  css  js  c++  java
  • NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

     

    上图是一个简要的NGUI的图形工作流程,UIGeometry被UIWidget实例化之后,通过UIWidget的子类,也就是UISprit,UILabel等,在OnFill()函数里算出所需的Geometry缓存(顶点数,UVColor,法线,切线)。PS:之所以要生成这些数据,是为了之后生成mesh来渲染

    UIPanel,通过遍历自己子类下所有的UIWidget组件(已经按深度排序),先创建一个UIDrawCall,然后把该Widget的material,texture,shader对象以及Geometry的缓存传给UIDrawCall,如此反复循环搜索该UIPanel下的每一个Widget,只要是material,texture,shader都和上一个Widget一样的Widget,他们的缓存都传给同一个UIDrawCall,直到循环结束或者碰到一个材质球,贴图,shader对象任一不相同的Widget。当遇到这种Widget,循环会再创建一个新的UIDrawCall,然后传递material,texture,shader,缓存,如此这般,直到循环完全结束。

    每次有新的UIDrawCall产生,UIPanel就会调用上一个UIDrawCall的UpdateGeometry()函数,来创建渲染所需的对象。这些对象分别是MeshFilter,MeshRender,和最重要的Mesh(Mesh的顶点,UVColor,法线,切线,还有三角面)。这些对象都会像我们正常在游戏中新建Cube一样,依附在创建UIDrawCall时生成的GameObject上以便可以渲染。我们在Editor中是看不到这个GameObject的,是因为创建的时候设置了HideFlags.HideAndDontSave。

     

    所以,NGUI的实际渲染流程,就是一个把Widget上的视觉组件生成的缓存,做成UIDrawCall之后,生成mesh来渲染的过程,很简单。

    如果您仅仅只是对NGUI的渲染过程感兴趣,那么看到这里就可以了,下面是一些技术性的问题。

     

    关于渲染顺序还有实际游戏中的NGUI造成的DrawCall

     

    在讨论渲染顺序之前,我们先大概了解一下在NGUI中,什么对渲染的层级有决定性的影响。

     

    A.Camera.Depth 不同相机之前的深度属性,在渲染顺序的优先度里面是最高的,Depth越大,渲染的图像越靠前,和空间无关。

    B.render.sortingOrder 一个render上的int属性,正常材质球调节这个属性没有什么反应,但是NGUI的材质球 Transparent Color 会受到这个属性的影响,值越大越靠前,和空间无关,可直接在UIPanel上设置。

    C.Render Queue 一个materialshader都有的属性,一个int值,意思是渲染队列,一般从3000开始,如果直接修改material的render queue,就会完全覆盖shader上的该属性。在之前的Widget遍历中,每次新生成UIDrawCall,就会把这个UIDrawCall对应的material的render queue加上1,所以不同UIDrawCall之间的排序靠的就是这个,越晚生成的UIDrawCall的render queue越大,也就越靠前,这个前置效果也和空间无关(注:每个UIDrawCall所调用的material仅仅只是一个副本,所以可以单独修改其render queue)

    D.顶点缓存序列的先后 这里说的是UIGeometry里传递的顶点(vertex)序列,这是一组根据Widget上的视觉组件生成的vertex(例如,一般的UISprit在simple模式下,会生成四个vertex,位置和你所看到那个编辑模式下scene视图里的可拖动锚点四个角一样,最后我会把UIDrawCall生成的mesh现实出来,一看就明白)这些vertex传入UIDrawCall之后,会计算出三角面,生成mesh。根据生成的三角面的顺序,也就是这些vertex传入的先后,NGUI的材质球会自绘制一种先后关系。后生成的面视觉上总是能在先生成的面前面,这种先后关系,在之前Widget遍历的时候就已经决定了,Widget深度越小,就会先被传递缓存,那么他提供的vertex就会排在生成列表的前面。这种效果仅能用于NGUI的材质球 Transparent Color.

    E.空间上的前后关系 这个就不用多说了,不过NGUI已经抛弃了这个方法。改成直接用深度来控制.除非你是相同的render queue,但是又不是同一个DrawCall(不是同一个MESH)。不过如果出现这种情况,证明你的NGUI使用的不规范。

     

    以上这些影响视觉先后效果的优先度是A>B>C>D>E的。但是这种顺序除了A以外,只适用于特殊的material(目前试过,NGUI的material,还有粒子material,这两个material都相互按照这个规则影响),也许这和shader里面的参数有关。正常物体,和NGUI材质球的关系,空间位置仅仅只有A>E,BCD似乎都没有影响。

     

    关于C我再说一点,就是遍历Widget的时候,就算往后碰到的Widget和之前拥有一样的material texture shader,它们依然会生成新的UIDrawCall(比如,12Widget和3号不一样,那么接下来的4号如果和1号2号相同,它也只能生成新的DrawCall)。这是为了确保层次关系的完全正确性。

     

    Widget更新对渲染的影响,以及Panel的Clip

     

    UIDrawCall.UpdateGeometry()这个函数仅有在Panel.FillDrawCall()和Panel.FillAllDrawCalls ()被调用。UpdateGeometry之前说过,就是将送进来的缓存处理成mesh的一个函数。因为每个DrawCall之对应一个Mesh,如果该DrawCall所属的Widget有改动,那么这个DrawCall就要通过UpdateGeometry修改新传入的缓存重绘才能更新效果。

     

    UIPanel.FillAllDrawCalls()调用的话基本是整个Panel重绘了,还好调用条件比较苛刻,除了第一次LateUpdate,之后若有新的Widget加入进来,并且深度不在之前DrallCall的范围内,或者用了新的matiral shader texture那么就会影响之前已经布好的UI秩序,就会被重绘。之前提到的遍历Panel下的所有Widget就是这个函数,调用的时候性能会损失很大。一般来讲,我们做UI如果都做成prefab,然后前后关系仅仅靠panel的sort order 去控制,那么很少有机会会在运行中调用到这个函数。说简单点,就是当有可能需要生成新的UIDrawCall或者剔除UIDrawCall的时候,就会触发这个函数,这个机制,和之前遍历Widget来生成DrawCall的原理以及目的都是一样的。

     

    UIPanel.FillDrawCall(UIDrawCall dc) 填充单独的DrawCall.一般只有少量的widget更新的时候 没必要更新所有的DrawCall(比如Label上的text有变化),只更新对应widgetDrawCall就好了.FillDrawCall()唯一的执行条件就是该DrawCall的isDirtytrue,isDirty被切换为true的条件有三大类:1.widget上的视觉组件被更新,调用widget.MarkAsChanged();2.widget的忽然被添加删除和移动;3.PanelALPHA被改动;

     

    Panel对视野外物体的剪切的流程是在UIDrawCall.OnWillRenderObject ()里完成的,使用的是NGUI shader的功能。

     

    小结

     

    A.NGUIPANELWidget的排序,如果没有设定好层次,很容易多出来莫名其妙的DrawCall,如果真的不是必须,那么相同material,texture,shader的Widget的深度应该是连续的。

    B.尽可能避免运行时触发FillAllDrawCalls(),制作的时候就把所有UI部件的Prefab做好,不要凭空乱生成UI元素。

    C.即使是不可避免的运行FillDrawCall(UIDrawCall dc)(这里说不可避免是肯定的),那么我们要避免过多面数的mesh的DrawCall被更新,经常动的部件,可以考虑单独做成DrawCall,牺牲1~2DrawCall换来可观的性能,还是值得的.

     

    附上NGUI的Mesh开启代码

     

    using UnityEngine;
    using System.Collections;
    using UnityEditor;
    
    public static class NGUIMESH {
    
        [MenuItem("NGUI/NguiMeshView")]
    
        static public void NguiMeshView()
        {
            foreach (var panel in UIPanel.list)
            {
                foreach(var dc in panel.drawCalls)
                {
                    if (dc.gameObject.hideFlags != HideFlags.DontSave)
                    {
                        dc.gameObject.hideFlags = HideFlags.DontSave;
                    }
                    else
                    {
                        dc.gameObject.hideFlags = HideFlags.HideAndDontSave;
                    }
                }
            }
        }
    }

    显示结果: 

  • 相关阅读:
    第二十九课 循环链表的实现
    第二十八课 再论智能指针(下)
    第二十七课 再论智能指针(上)
    第二十六课 典型问题分析(Bugfix)
    普通new和placement new的重载
    leetcode 581. Shortest Unsorted Continuous Subarray
    leetcode 605. Can Place Flowers
    leetcode 219. Contains Duplicate II
    leetcode 283. Move Zeroes
    leetcode 217. Contains Duplicate
  • 原文地址:https://www.cnblogs.com/Persona/p/4149086.html
Copyright © 2011-2022 走看看