在项目的开发过程中,很多做过UI的同学估计都会遇到NGUI与unity3d物体的交叉显示问题,不知道如何处理,或者各种各样的界面穿插问题,界面层级混乱,对于界面来说,这些应该算是一个很严重的问题。在之前的一个预演项目在界面需求时,就曾遇到这样的问题,想把一个美术特效放在两个不同层级的Sprite中间显示,或者一个面板中显示了粒子效果后,再打开一个面板时,粒子效果不能穿透显示到新的面板,当时的解决方案只是解决了两个面板之间不会穿透显示,并没有很好的解决两个两个不同层级的Sprite之间显示。在这里,结合我们项目中实际使用的一种解决方式,来分析如何方便快捷的解决这个问题。

 

       为了深入理解这个问题,我们先来分析下NGUI的渲染流程和unity渲染顺序控制方式,然后介绍如何实现NGUI与模型和粒子特效穿插层级。

 

1: NGUI的渲染流程

在UI制作的过程中,我们在UIPanel中将一个个UISprite、UILabel等组件拼装、放置好。UIPanel作为总体控制,将组件显示出来,而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的顶点,UV,Color,法线,切线,还有三角面)。这些对象都会像我们正常在游戏中新建Cube一样,依附在创建UIDrawCall时生成的GameObject上以便可以渲染。我们在Editor中是看不到这个GameObject的,是因为创建的时候设置了HideFlags.HideAndDontSave。所以,NGUI的实际渲染流程,就是一个把Widget上的视觉组件生成的缓存,做成UIDrawCall之后,生成mesh来渲染的过程。

 

了解了渲染流程之后,我们在来看看NGUI中,什么对渲染的层级有决定性的影响。

 

A:Depth NGUI中最正统的控制panel之间层级关系的就是它的 depth 属性。depth越大,越靠后渲染,越在前面显示。

B:sortingOrder  ,一个render上的int属性,值越大越靠前,和空间无关,可直接在UIPanel上设置。

 

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

我们可以在UIPanel的Render Q属性中修改这个值

D: 顶点缓存序列的先后 ,取决与每个组件的Depth属性值,

UIGeometry里传递的顶点(vertex)序列,这是一组根据Widget上的视觉组件生成的vertex,这些vertex传入UIDrawCall之后,会计算出三角面,生成mesh。根据生成的三角面的顺序,也就是这些vertex传入的先后,NGUI的材质球会自绘制一种先后关系。后生成的面视觉上总是能在先生成的面前面,这种先后关系,在之前Widget遍历的时候就已经决定了,Widget深度越小,就会先被传递缓存,那么他提供的vertex就会排在生成列表的前面

E: 空间深度 
   在摄像机坐标系下的Z轴,控制着该相机下的物体的深度,在fragment shader中进行深度测试,这样就控制了渲染到屏幕的顺序。

 

2:Unity3D对象的渲染顺序

   默认情况下,Unity会基于对象距离摄像机的远近来排序你的对象。因此,当一个对象离摄像机越近,它就会优先绘制在其他更远的对象上面。对于大多数情况这是有效并合适的,但是在一些特殊情况下,你可能想要自己控制对象的绘制顺序。

       Unity提供给我们一些默认的渲染队列,每一个对应一个唯一的值,来指导Unity绘制对象到屏幕上。这些内置的渲染队列被称为Background, Geometry, AlphaTest, Transparent, Qverlay。这些队列不是随便创建的,它们是为了让我们更容易地编写Shader并处理实时渲染的。下图的表格描述了这些渲染队列的用法:

因此,一旦你知道你的对象属于哪一个渲染队列,就可以指定它的内置渲染队列标签,重写对象的深度排序。

但是有一点需要注意,就是不透明物体渲染时会进行深度检测(ZTest),深度缓存会记录距离摄像机最近的顶点,大于深度缓存的顶点会被抛弃,直接跳过渲染过程。如果顶点深度小于深度缓存中的数据,则更新深度缓存,将当前顶点的深度写入深度缓存(ZWrite)。

想在屏幕上渲染透明物体,需要在shader中将Queue设置为大于3000的值,并将ZWrite设置为Off,因为透明物体只会叠加在屏幕原有色值上而不是将其遮挡。因此透明物体的渲染结果只受其渲染顺序的影响,其渲染顺序为根据custom render queue从小到大,custom render queue相等时到摄像头距离由远及近依次渲染。对于透明物体虽然关闭了ZWrite,但ZTest依然有效,其还是会被Z值更小的不透明物体遮挡。

3:  NGUI中让Unity3d物体交叉显示

经过对NGUI和unity3d物体的渲染顺序进行分析后,经明白了各个地方是可以怎么控制渲染顺序了,接下来就来解决一些项目中遇到的问题。

先从第一个简单问题说起,比如两个面板之间显示一个粒子效果,粒子特效本身是用“点精灵”渲染的,每个粒子就是一个点精灵,可以看做一个片模型,而片模型就可以通过设置Sorting Order属性来修改显示层级。sorting Order默认值为0,现有PanelA、PanelB两个界面,把PanelA设置为0,PanelA设置2:

PanelA:0 

粒子特效:1 
PanelB:2

粒子特效刚好插在A、B之间,显示效果也是粒子特效穿插在A、B之间,感觉很轻松的就解决了上面提到的问题,但是这样有一个严重的问题,就是当面板很多时,无法确保这个粒子效果不会穿透面板PanelC。但是你也可以通过统一控制面板的显示规则,来控制每个面板之间的层级间隔,来避免这种情况出现,在这里就不作讨论了。

 

然后我们另一种思路来解决这一问题:直接控制粒子特效的render queue值,来达到使得UI、特效按照我们希望的顺序进行渲染的目的,毕竟NGUI中也大量使用了RenderQueue来控制前后关系,比如在UIPanel的LateUpdate方法中的具体实现:

可以看到三种模式下,RenderQueue具体值加的方式,一般我们都是用Automatic模式,这种模式下是根据每个UIPanel中生成的DrawCall自动计算RenderQueue的值,而在一个面板中,可能存在一到多个DrawCall,在为每个归属于不同层次的widget指定了所属的render queue顺序之后,剩下的就是为特效指定应归属的render queue。我们项目中目前的实现方式也是按照这种方式来实现的,下面来看下具体实现过程:

根据实际遇到的问题,一个特效有可能现在在整个面板的前或者后面、或者具体某一个组件的前面或者后面,这样我们引入UIEffectRenderQueue.cs脚本,来指定当前特效展示的方式:

//指定一个作为target的widget
    publicUIWidget mRenderQueueWidgetTarget = null;
    //指定一个作为target的Panel
    publicUIPanel mRenderQueuePanelTarget = null;
    //target的当前RenderQueue,如果在target的前面或者后面显示,该值会被修改
    publicint m_targetRenderQueue = -1;
    //是否要翻转Z轴
    publicbool m_reverseZOrder = false;
    //是否立即生效
    publicbool m_immeApply = false;
    //显示顺序
    publicRenderType m_type = RenderType.FRONT;
显示顺序的定义:
    publicenumRenderType
    {
        FRONT,//显示在目标组件的前面
        BACK, //显示在目标组件的后面
        EQUAL, //显示在目标组件的同级
     }
 
脚本生效后的核心方法如下:
privatevoid Apply()
    {
        float currZOrder = 0f;
        int currAttachType = -1;
        int queue = GetDestRenderQueue();
        if (queue <= 0)
            return;
        if (m_lastRenderQueue != queue)
        {
            Renderer renderer;
            OrderMaterial mat;
            int sortingOrder = GetDestSortingOrder();
            for (int i = 0; i < materials.Count; ++i)
            {
                mat = materials[i];
                renderer = mat.render;
                if (renderer)
                {
                    if (renderer.sortingOrder != sortingOrder)
                        renderer.sortingOrder = sortingOrder;
                }
                materials[i] = SetZOrderOrderMaterial(ref mat);
            }
            SortZOrder();
 
            queue = GetDestRenderQueue();
            m_lastRenderQueue = queue;
 
            if (m_type == RenderType.FRONT)
            {
                for (int i = 0; i < materials.Count; ++i)
                {
                    mat = materials[i];
                    if (currAttachType != materials[i].attachType || currZOrder != materials[i].zOrder)
                    {
                        queue += 1;
                        currZOrder = materials[i].zOrder;
                        currAttachType = materials[i].attachType;
                    }
                    materials[i] = ApplyOrderMaterial(ref mat, queue);
                }
            }
            elseif (m_type == RenderType.BACK)
            {
                for (int i = materials.Count - 1; i >= 0; --i)
                {
                    mat = materials[i];
                    if (currAttachType != materials[i].attachType || currZOrder != materials[i].zOrder)
                    {
                        queue -= 1;
                        currZOrder = materials[i].zOrder;
                        currAttachType = materials[i].attachType;
                    }
                    materials[i] = ApplyOrderMaterial(ref mat, queue);
                }
            }
           
        }
}
获取目标Target的RenderQueue的具体实现如下:
privateint GetDestRenderQueue()
{
        int queue = m_targetRenderQueue;
        if (m_type == RenderType.FRONT || m_type == RenderType.BACK)
        {
            if (mRenderQueuePanelTarget != null)
            {
                if (m_type == RenderType.FRONT)
                {
                    queue = mRenderQueuePanelTarget.startingRenderQueue +
                        mRenderQueuePanelTarget.drawCalls.Count +
                        mRenderQueuePanelTarget.mAdditionalDrawCallCounts;
                }
                elseif (m_type == RenderType.BACK)
                {
                    queue = mRenderQueuePanelTarget.startingRenderQueue;
                }
            }
            if (mRenderQueueWidgetTarget != null)
            {
                if (mRenderQueueWidgetTarget.drawCall != null)
                {
                    queue = mRenderQueueWidgetTarget.drawCall.renderQueue;
                }
                else
                {
                    queue = 2000;
                }
            }
            if (queue>0)
                queue += m_type == RenderType.FRONT ? 1 : -1;
        }
        return queue;
}
获取目标Target的SortingOrder的具体实现如下:
    privateint GetDestSortingOrder()
    {
        int sortingOrder = 0;
        if (mRenderQueuePanelTarget != null)
        {
            sortingOrder = mRenderQueuePanelTarget.sortingOrder;
        }
        if (mRenderQueueWidgetTarget != null && mRenderQueueWidgetTarget.drawCall != null)
        {
            sortingOrder = mRenderQueueWidgetTarget.drawCall.sortingOrder;
        }
        return sortingOrder;
}

   原理上就是直接修改这一特效下所有renderer组建中的material的renderQueue值,来按照需要指定该特效需要显示在哪一个层级。在具体的实现中,有一个小的细节,就是在修改renderQueue的同时,也修改了sorting order,是因为UIPanel的depth控制着UIDrawCall的生成顺序,影响了RenderQueue的顺序,而sorting order比RenderQueue优先级更高,所以为了渲染效果的准确性,在设置renderQueue的同时,也需要把sorting order设置为Target的order值。