NGUI与Unity3d物体交叉显示的一种解决方案
在项目的开发过程中,很多做过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 ,一个material和shader都有的属性,一个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值。