NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

时间:2023-12-15 23:38:44

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

上图是一个简要的NGUI的图形工作流程,UIGeometry被UIWidget实例化之后,通过UIWidget的子类,也就是UISprit,UILabel等,在OnFill()函数里算出所需的Geometry缓存(顶点数,UV,Color,法线,切线)。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的顶点,UV,Color,法线,切线,还有三角面)。这些对象都会像我们正常在游戏中新建Cube一样,依附在创建UIDrawCall时生成的GameObject上以便可以渲染。我们在Editor中是看不到这个GameObject的,是因为创建的时候设置了HideFlags.HideAndDontSave。

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

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

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

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

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

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

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

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

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

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)

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

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

NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

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(比如,1,2号Widget和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有变化),只更新对应widget的DrawCall就好了.FillDrawCall()唯一的执行条件就是该DrawCall的isDirty为true,isDirty被切换为true的条件有三大类:1.widget上的视觉组件被更新,调用widget.MarkAsChanged();2.widget的忽然被添加删除和移动;3.Panel的ALPHA被改动;

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

小结

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

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

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

附上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;
}
}
}
}
}

显示结果: NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)