首页 > 代码库 > NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)
NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)
上图是一个简要的NGUI的图形工作流程,UIGeometry被UIWidget实例化之后,通过UIWidget的子类,也就是UISprit,UILabel等,算出所需的Geometry缓存(顶点数,UV,Color,法线,切线)。
而UIPanel,通过遍历自己子类下所有的UIWidget组件(已经按深度排序),先创建一个UIDrawCall,把Widget的material,texture,shader对象传递给该UIDrawCall,然后把该Widget里的Geometry缓存传给UIDrawCall,反复循环,只要是material,texture,shader都和上一个Widget一样的Widget,他们的缓存都传给同一个UIDrawCall,直到循环结束或者碰到一个材质球,贴图,shader对象任一不相同的Widget。当遇到这种Widget,循环会再创建一个新的UIDrawCall,然后传递material,texture,shader,缓存,如此这般,直到循环完全结束。
每次有新的UIDrawCall产生,上一个UIDrawCall都会执行UpdateGeometry()函数,来创建渲染所需的对象。这些对象是MeshFilter,MeshRender,Mesh(Mesh的顶点,UV,Color,还有最重要的三角面)。这些对象都会像我们正常在游戏中新建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 会受到这个属性的影响,值越大越靠前,和空间无关
C.Render Queue 一个material和shader都有的属性,渲染队列,一般从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.空间上的前后关系 这个就不用多说了
以上这些影响视觉先后效果的优先度是A>B>C>D>E的
关于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修改新传入的缓存重绘才能更新效果。
FillAllDrawCalls()调用的话基本是整个Panel重绘了,还好调用条件比较苛刻,除了第一次LateUpdate,之后若有新的Widget加入进来,并且深度不在之前DrallCall的范围内,或者用了新的matiral shader texture那么就会影响之前已经布好的UI秩序,就会被重绘。之前提到的遍历Panel下的所有Widget就是这个函数,调用的时候性能会损失很大。一般来讲,我们做UI如果都做成prefab,然后前后关系仅仅靠panel的sort order 去控制,那么很少有机会会在运行中调用到这个函数。说简单点,就是当有可能需要生成新的UIDrawCall或者剔除UIDrawCall的时候,就会触发这个函数,这个机制,和之前遍历Widget来生成DrawCall的原理以及目的都是一样的。
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)