NGUI所見即所得之UIPanel

NGUI所見即所得之UIPanel

       

       之前在NGUI所見即所得之UIWidget , UIGeometry & UIDrawCall 文中就這樣用過這樣的一個例子:

 

                UIGeometry好比爲煮菜準備食材,UIDrawCall好比是煮菜的工具(鍋,爐子等),UIPanel就是大廚了決定着什麼時候該煮菜,UIWidget(UILabel,UISprite和UITexture)是這道菜怎麼樣的最終呈現。

         本來不打算繼續寫UIPanel的內容的,因爲沒有這麼深刻的需求,後面自己根據FastGUI生成的UI發現DrawCall數很多:

       一個很簡單的界面竟然用了9個DrawCall,相同的material沒有進行DrawCall完全的合併,相對於以前一個Material一個DrawCall是不可接受的,所以這樣就很必要去看下UIPanel都做了哪些事情,看下了NGUI的更新日誌有這樣的一句話:

       3.0.0:

       - NEW: Changed the way widgets get batched, properly fixing all remaining Z/depth issues.

       - NEW: Draw calls are now automatically split up as needed (no more sandwiching issues!)

       NGUI之前的版本關於組件的顯示跟Z周,depth以及圖集和UIPanel的關係一直都受到大家吐槽和詬病(尤其夾層問題),所以NGUI3.0.3就徹底解決這個問題:使用DrawCall切割,然後由depth完全決定組件顯示的前後。

 

        也就是說,NGUI對DrawCall進行了分割處理,導致DrawCall數量“劇烈”增加,所以要解決DrawCall數量增加,就要UIPanel產生一個UIDrawCall的原理,然後減少UIDrawCall的生成或進行合併。

        從上圖,可以發現NGUI還是對部分組件進行了UIDrawCall合併——多個UIWidget使用同一個UIDrawCall,所以要想做到同一個Material使用一個UIDrawCall理論上是完全可行的。

 

再說UIWidget,UIGeometry&UIDrawCall

        雖然已經有NGUI所見即所得之UIWidget , UIGeometry & UIDrawCall 一文,但是由於之前是在幾乎忽略UIPanel的情況下理順UIWiget,UIGeometry&UIDrawCall三者的關係的,所以文中的組織邏輯比較混亂,條理不強,加上本文也是建立者三者之上的,作爲行爲的結構的流暢性和完整性,所以還是在簡要交代下。
       上圖是UIWidget,UIGeometry&UIDrawCall的關係圖,UIWidget用於UIDrawcall mDrawCall和UIGeometry mGeo兩個成員變量,其中UIGeometry就是對UIWidget的頂點vertices,uvs和color進行存儲和更新,UIDrawCall就是根據提供的數據(統一在UIPanel指派)進行渲染繪製。


        
UIGeometry完全由UIWidget維護,首先UILabel,UISprite,UITexture對UIWidget的OnFill進行重寫——初始化mGeo的verts,uvs,cols的BetterList。然後UIWidget的UpdateGeometry函數對UIGeometry的ApplyTransform()和WriteToBuffer()調用進行更新。

 

         每一個UIWidget都有一個UIGeometry,但是並不都有一個UIDrawCall,而是要通過Batch合併達到減少DrawCall的數量,UIDrawCall是由UIPanel生成的。至於什麼是DrawCall,因爲沒有3D引擎經驗,只能從隻言片語中拾獲一點理解:

       

             “Unity(或者說基本所有圖形引擎)生成一幀畫面的處理過程大致可以這樣簡化描述:引擎首先經過簡單的可見性測試,確定攝像機可以看到的物體,然後把這些物體的頂點(包括本地位置、法線、UV等),       索引(頂點如何組成三角形),變換(就是物體的位置、旋轉、縮放、以及攝像機位置等),相關光源,紋理,渲染方式(由材質/Shader決定)等數據準備好,然後通知圖形API——或者就簡單地看作是通知GPU       ——開始繪製,GPU基於這些數據,經過一系列運算,在屏幕上畫出成千上萬的三角形,最終構成一幅圖像。 在Unity中,每次引擎準備數據並通知GPU的過程稱爲一次Draw Call。這一過程是逐個物體進行的,對       於每個物體,不只GPU的渲染,引擎重新設置材質/Shader也是一項非常耗時的操作。因此每幀的Draw Call次數是一項非常重要的性能指標。”

     

       NGUI被說的最多的優點就是:減少DrawCall數量。但現在爲了解決sandwiching issues和Z/depth issues,對DrawCall進行split。

 

NGUI指派DrawCall的原理

   

       前面說到,UIDrawCall是由UIPanel生成指派的,哪些UIWiget共用(也就是Batch)一個DrawCall在UIPanel中決定的。UIDrawCall有一個靜態變量:

C#代碼  收藏代碼
  1. /// <summary>  
  2. /// All draw calls created by the panels.  
  3. /// </summary>  
  4. static public BetterList<UIDrawCall> list = new BetterList<UIDrawCall>();  

      也就是說所有的UIDrawCall都會保存在list中,都說“大蛇要打七寸”,只要找到哪裏有 list.add 的調用就知道生成增加了一個UIDrawCall,這樣就找到GetDrawCall函數(也可以通過MonoBehaviour的調試功能打斷點進行函數跟蹤):

C#代碼  收藏代碼
  1.     /// <summary>  
  2.     /// Get a draw call at the specified index position.  
  3.     /// </summary>  
  4.   
  5.     UIDrawCall GetDrawCall (int index, Material mat)  
  6.     {  
  7.         if (index < UIDrawCall.list.size)  
  8.         {  
  9.             UIDrawCall dc = UIDrawCall.list.buffer[index];  
  10.   
  11.             // If the material and texture match, keep using the same draw call  
  12.             if (dc != null && dc.panel == this && dc.baseMaterial == mat && dc.mainTexture == mat.mainTexture) return dc;  
  13.   
  14.             // Otherwise we need to destroy all the draw calls that follow  
  15.             for (int i = UIDrawCall.list.size; i > index; )  
  16.             {  
  17.                 UIDrawCall rem = UIDrawCall.list.buffer[--i];  
  18.                 DestroyDrawCall(rem, i);  
  19.             }  
  20.         }  
  21.  
  22. #if UNITY_EDITOR  
  23.         // If we're in the editor, create the game object with hide flags set right away  
  24.         GameObject go = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags("_UIDrawCall [" + mat.name + "]",  
  25.             //HideFlags.DontSave | HideFlags.NotEditable);  
  26.             HideFlags.HideAndDontSave);  
  27. #else  
  28.         GameObject go = new GameObject("_UIDrawCall [" + mat.name + "]");  
  29.         DontDestroyOnLoad(go);  
  30. #endif  
  31.         go.layer = cachedGameObject.layer;  
  32.           
  33.         // Create the draw call  
  34.         UIDrawCall drawCall = go.AddComponent<UIDrawCall>();  
  35.         drawCall.baseMaterial = mat;  
  36.         drawCall.renderQueue = UIDrawCall.list.size;  
  37.         drawCall.panel = this;  
  38.         //Debug.Log("Added DC " + mat.name + " as " + UIDrawCall.list.size);  
  39.         UIDrawCall.list.Add(drawCall);  
  40.         return drawCall;  
  41.     }  

       進一步找到Fill()的調用:

C#代碼  收藏代碼
  1. /// <summary>  
  2. /// Fill the geometry fully, processing all widgets and re-creating all draw calls.  
  3. /// </summary>  
  4.   
  5. static void Fill ()  
  6. {  
  7.     for (int i = UIDrawCall.list.size; i > 0; )  
  8.         DestroyDrawCall(UIDrawCall.list[--i], i);  
  9.   
  10.     int index = 0;  
  11.     UIPanel pan = null;  
  12.     Material mat = null;  
  13.     UIDrawCall dc = null;  
  14.   
  15.     for (int i = 0; i < UIWidget.list.size; )  
  16.     {  
  17.         UIWidget w = UIWidget.list[i];  
  18.   
  19.         if (w == null)  
  20.         {  
  21.             UIWidget.list.RemoveAt(i);  
  22.             continue;  
  23.         }  
  24.   
  25.         if (w.isVisible && w.hasVertices)  
  26.         {  
  27.             if (pan != w.panel || mat != w.material)    //a)  
  28.             {  
  29.                 if (pan != null && mat != null && mVerts.size != 0)  
  30.                 {  
  31.                     pan.SubmitDrawCall(dc);  
  32.                     dc = null;  
  33.                 }  
  34.   
  35.                 pan = w.panel;  
  36.                 mat = w.material;  
  37.             }  
  38.   
  39.             if (pan != null && mat != null)   //b)  
  40.             {  
  41.                 if (dc == null) dc = pan.GetDrawCall(index++, mat);  
  42.                 w.drawCall = dc;  
  43.                 if (pan.generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);  
  44.                 else w.WriteToBuffers(mVerts, mUvs, mCols, nullnull);  
  45.             }  
  46.         }  
  47.         else w.drawCall = null;  
  48.         ++i;  
  49.     }  
  50.   
  51.     if (mVerts.size != 0)  
  52.         pan.SubmitDrawCall(dc);  
  53. }  

 整理Fill函數的原理如下r:

 

       (1) 獲取UIWidget的隊列UIWidget.list(已經根據depth排好序),聲明一個UIPanel pan,Material mat和UIDrawCall dc,pan,mat和dc都是保存上一次循環的UIPanel,Material和UIDrawCall。

       (2) 遍歷UIWidget.list,循環體中對 當前UIWiget w的panel和material是否和當前pan,mat是否相同 進行判斷,分爲兩種情況:

                  a)如果有一種不相同,調用SubmitDrawCall函數,SubmitDrawCall函數其實就是使用pan的mVerts, mUvs, mCols數據,調用UIDrawCall的set函數對Mesh,MeshRender,MeshFilter等進行“設置組裝”。

                  b)如果相同,通過調用GetDrawCall獲取當前pan和mat的DrawCall,然後將UIWidget w的UIGeometry數據放入mVerts, mUvs, mCols(通過調用函數w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans))  

 

     小結:UIPanel的mVerts,mUVs,mCols只是要將要傳給UIDrawCall數據的一個“積蓄”過渡的一個概念,也就是說,Fill函數式這麼操作的:先將UIWidget w的中UIGeometry的數據緩存在UIPanel的mVerts,mUVs,mCols,只有當不能再pan或mat與當前的w.panel或w.material不同時就不能再緩存了,然後通過SubmitDrawCall,生成UIDrawCall的工作才完成,然後再重新 new 一個新的UIDrawCall繼續緩存數據。

 

UIPanel完整工作流程——LateUpdate

     前面介紹UIDrawCall的產生過程,當然這是UIPanel最重要的工作之一,在對UIDrawCall進行更新是要對UIPanel的其他信息(transform,layer,widget)等進行更新:

C#代碼  收藏代碼
  1. /// <summary>  
  2. /// Main update function  
  3. /// </summary>  
  4.   
  5. void LateUpdate ()  
  6. {  
  7.     // Only the very first panel should be doing the update logic  
  8.     if (list[0] != thisreturn;  
  9.   
  10.     // Update all panels  
  11.     for (int i = 0; i < list.size; ++i)  
  12.     {  
  13.         UIPanel panel = list[i];  
  14.         panel.mUpdateTime = RealTime.time;  
  15.         panel.UpdateTransformMatrix();  
  16.         panel.UpdateLayers();  
  17.         panel.UpdateWidgets();  
  18.     }  
  19.     // Fill the draw calls for all of the changed materials  
  20.     if (mFullRebuild)  
  21.     {  
  22.         UIWidget.list.Sort(UIWidget.CompareFunc);  
  23.         Fill();  
  24.     }  
  25.     else  
  26.     {  
  27.         for (int i = 0; i < UIDrawCall.list.size; )  
  28.         {  
  29.             UIDrawCall dc = UIDrawCall.list[i];  
  30.   
  31.             if (dc.isDirty)  
  32.             {  
  33.                 if (!Fill(dc))  
  34.                 {  
  35.                     DestroyDrawCall(dc, i);  
  36.                     continue;  
  37.                 }  
  38.             }  
  39.             ++i;  
  40.         }  
  41.     }  
  42.   
  43.     // Update the clipping rects  
  44.     for (int i = 0; i < list.size; ++i)  
  45.     {  
  46.         UIPanel panel = list[i];  
  47.         panel.UpdateDrawcalls();  
  48.     }  
  49.     mFullRebuild = false;  
  50. }  

        就不進行文字描述了,貼一張自己的畫的LateUpdate()函數調用棧圖(不光文筆不好,畫圖也不行,硬傷呀,就這樣也是琢磨很久畫的):

DrawCall數量優化

         言歸正傳,本文的話題就是對於NGUI3.0.4的版本(目前最新版)如何減少DrawCall, 先回到文中的第一幅圖,發現兩個以New atlas圖集爲material的DrawCall夾着一個以font爲字體集的DrawCall間隔,然後使用MonoBehaviour的斷點調試功能進行跟蹤得到UIWidget.list隊列:

 

        發現一個規律:使用相同material的連續UIWidget(UILabel,UISprite)共用一個UIDrawCall。這樣就給了一個解決策略:對UIWidget.list進行排序,使得使用相同的material的UIWidget在UIWidget.list相連,而UIWidget.list是根據UIWidget的depth進行排序的。所以可以有如下兩種方法:

        1)修改UIWidget(UILabel,UISprite)的depth,限定好UIWidget.list的排序

        2)重寫UIWidget的CompareFunc方法。

C#代碼  收藏代碼
  1. /// <summary>  
  2. /// Static widget comparison function used for depth sorting.  
  3. /// </summary>  
  4.   
  5. static public int CompareFunc (UIWidget left, UIWidget right)  
  6. {  
  7.     int val = UIPanel.CompareFunc(left.mPanel, right.mPanel);  
  8.   
  9.     if (val == 0)  
  10.     {  
  11.         if (left.mDepth < right.mDepth) return -1;  
  12.         if (left.mDepth > right.mDepth) return 1;  
  13.   
  14.         Material leftMat = left.material;  
  15.         Material rightMat = right.material;  
  16.   
  17.         if (leftMat == rightMat) return 0;  
  18.         if (leftMat != nullreturn -1;  
  19.         if (rightMat != nullreturn 1;  
  20.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;  
  21.     }  
  22.     return val;  
  23. }  

         下面對原來對屬於第三個DrawCall的兩個UILabel增大他們的depth,發現DrawCall立馬減少一個了,說明這個方法是可行的:
 
 

 

    

 

 

 

 

 

        同理,重寫UIWidget的CompareFunc也是可以的,按照Material的name優先排序,只有當material一樣是才考慮depth進行排序:

C#代碼  收藏代碼
  1. /// <summary>  
  2. /// Static widget comparison function used for depth sorting.  
  3. /// </summary>  
  4.   
  5. static public int CompareFunc (UIWidget left, UIWidget right)  
  6. {  
  7.     int val = UIPanel.CompareFunc(left.mPanel, right.mPanel);  
  8.   
  9.     if (val == 0)  
  10.     {  
  11.         //原理排序的方法  
  12.         /*if (left.mDepth < right.mDepth) return -1; 
  13.         if (left.mDepth > right.mDepth) return 1; 
  14.  
  15.         Material leftMat = left.material; 
  16.         Material rightMat = right.material; 
  17.  
  18.         if (leftMat == rightMat) return 0; 
  19.         if (leftMat != null) return -1; 
  20.         if (rightMat != null) return 1; 
  21.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;*/  
  22.           
  23.   
  24.         Material leftMat = left.material;  
  25.         Material rightMat = right.material;  
  26.   
  27.         if (leftMat == rightMat)   
  28.         {  
  29.             if (left.mDepth < right.mDepth) return -1;  
  30.             else if (left.mDepth > right.mDepth) return 1;  
  31.             else return 0;  
  32.         }  
  33.         if(leftMat !=null & rightMat != null)  
  34.             return string.Compare(leftMat.name,rightMat.name);  
  35.         if (leftMat != nullreturn -1;  
  36.         if (rightMat != nullreturn 1;  
  37.         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;  
  38.           
  39.     }  
  40.     return val;  
  41. }  

       最終的DrawCall數量一定是等於使用的Material的數量:

 

 

 

還是夾層問題(sandwiching issues!)

       現在我們完全可以實現一個Material一個DrawCall,但是這樣還是沒有解決夾層的難題,NGUI給我們解決方法就是多一個DrawCall,這個其實跟3.0之前的版本多用一個UIPanel或者UIAtlas是一樣的道理。這樣還是感覺沒有從本質上解決這個問題,只是換了一種方式權衡了一下。

       記得NGUI3.0之前的版本還是有Z軸的概念的,現在Z軸完全是形同虛設,但是3D引擎的圖形一定是跟Z軸是密切的關係的,而最終的圖形顯示的位置關係是由Mesh的頂點決定的,所以可以考慮Z軸來解決夾層問題:DrawCall控制的渲染隊列的次序renderQueue,Mesh控制的是實際繪製的“地理位置”,如下圖所示,A和C使用相同的圖集有相同的material,B單獨使用一個圖集,可以通過material來排序或者定製好depth,讓A和C使用一個DrawCall,但是C的Mesh(參考transform的Z軸)會在B的“前面”,這樣就應該可以實現夾層的效果了。

       做了下測試,修改Mesh的Z軸沒有什麼變化,然後早上向同事請教了,因爲Material使用的Shader使用了透明,這樣就不能做深度測試,也就是Mesh的“深度”是不影響的,這樣最終的顯示就跟Shader的renderQueue有關了,即renderQueue越大,顯示的越靠前面(重疊的圖層,renderQueue越大,越靠前)。當然現在只有增加一個DrawCall(如多使用一個UIPanel或用另外一個Material)來做到Material的夾層效果。
        

 

小結:

        NGUI更新的很快,之前一直也沒有仔細研究,最近開始慢慢看了些,也寫了些博客,主要有3點收穫:1)NGUI的渲染機制,2)NGUI相關“組件”(Font,Atlas,UIWidget等)實現方法,3)NGUI的設計模式。當然D.S.Qiu覺得NGUI作爲一個大的系統一定會有冗餘和詬病,使用了很多“緩存”的思想,很多細節都沒有處理好,所以我們都可以再努力完善,爭取做“站在巨人的肩膀上”的那個人。

        發現沒有3D引擎以及圖形渲染的基礎,做點事情還是很蹩腳的,GPU的處理輸出,顯存的大小,CPU與GPU的交互都是要考慮的,有空的時候還是要找到這方面的書來惡補下……

 

       如果您對D.S.Qiu有任何建議或意見可以在文章後面評論,或者發郵件([email protected])交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。

        轉載請在文首註明出處:http://dsqiu.iteye.com/blog/1973651

更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)

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