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

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

 

上圖是一個簡要的NGUI的圖形工作流程,UIGeometry被UIWidget實例化之後,通過UIWidget的子類,也就是UISprit,UILabel等,在OnFill()函數裏算出所需的Geometry緩存(頂點數,UVColor,法線,切線)。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的頂點,UVColor,法線,切線,還有三角面)。這些對象都會像我們正常在遊戲中新建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 一個materialshader都有的屬性,一個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使用的不規範。

 

以上這些影響視覺先後效果的優先度是A>B>C>D>E的。但是這種順序除了A以外,只適用於特殊的material(目前試過,NGUI的material,還有粒子material,這兩個material都相互按照這個規則影響),也許這和shader裏面的參數有關。正常物體,和NGUI材質球的關係,空間位置僅僅只有A>E,BCD似乎都沒有影響。

 

關於C我再說一點,就是遍歷Widget的時候,就算往後碰到的Widget和之前擁有一樣的material texture shader,它們依然會生成新的UIDrawCall(比如,12Widget和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有變化),只更新對應widgetDrawCall就好了.FillDrawCall()唯一的執行條件就是該DrawCall的isDirtytrue,isDirty被切換爲true的條件有三大類:1.widget上的視覺組件被更新,調用widget.MarkAsChanged();2.widget的忽然被添加刪除和移動;3.PanelALPHA被改動;

 

Panel對視野外物體的剪切的流程是在UIDrawCall.OnWillRenderObject ()裏完成的,使用的是NGUI shader的功能。

 

小結

 

A.NGUIPANELWidget的排序,如果沒有設定好層次,很容易多出來莫名其妙的DrawCall,如果真的不是必須,那麼相同material,texture,shader的Widget的深度應該是連續的。

B.儘可能避免運行時觸發FillAllDrawCalls(),製作的時候就把所有UI部件的Prefab做好,不要憑空亂生成UI元素。

C.即使是不可避免的運行FillDrawCall(UIDrawCall dc)(這裏說不可避免是肯定的),那麼我們要避免過多面數的mesh的DrawCall被更新,經常動的部件,可以考慮單獨做成DrawCall,犧牲1~2DrawCall換來可觀的性能,還是值得的.

 

附上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;
                }
            }
        }
    }
}
複製代碼

顯示結果: 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章