上圖是一個簡要的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的實際渲染流程,就是一個把Widget上的視覺組件生成的緩存,做成UIDrawCall之后,生成mesh來渲染的過程,很簡單。
如果您僅僅只是對NGUI的渲染過程感興趣,那么看到這里就可以了,下面是一些技術性的問題。
關於渲染順序還有實際游戲中的NGUI造成的DrawCall
在討論渲染順序之前,我們先大概了解一下在NGUI中,什么對渲染的層級有決定性的影響。
A.Camera.Depth 不同相機之前的深度屬性,在渲染順序的優先度里面是最高的,Depth越大,渲染的圖像越靠前,和空間無關。
B.render.sortingOrder 一個render上的int屬性,正常材質球調節這個屬性沒有什么反應,但是NGUI的材質球 Transparent Color 會受到這個屬性的影響,值越大越靠前,和空間無關,可直接在UIPanel上設置。
C.Render Queue 一個material和shader都有的屬性,一個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使用的不規范。
關於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; } } } } }
顯示結果: