国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

NGUI所見即所得之UIPanel

2019-11-09 17:31:07
字體:
來源:轉載
供稿:網友

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#代碼  收藏代碼/// <summary>  /// All draw calls created by the panels.  /// </summary>  static public BetterList<UIDrawCall> list = new BetterList<UIDrawCall>();  

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

C#代碼  收藏代碼    /// <summary>      /// Get a draw call at the specified index position.      /// </summary>        UIDrawCall GetDrawCall (int index, Material mat)      {          if (index < UIDrawCall.list.size)          {              UIDrawCall dc = UIDrawCall.list.buffer[index];                // If the material and texture match, keep using the same draw call              if (dc != null && dc.panel == this && dc.baseMaterial == mat && dc.mainTexture == mat.mainTexture) return dc;                // Otherwise we need to destroy all the draw calls that follow              for (int i = UIDrawCall.list.size; i > index; )              {                  UIDrawCall rem = UIDrawCall.list.buffer[--i];                  DestroyDrawCall(rem, i);              }          }   #if UNITY_EDITOR          // If we're in the editor, create the game object with hide flags set right away          GameObject go = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags("_UIDrawCall [" + mat.name + "]",              //HideFlags.DontSave | HideFlags.NotEditable);              HideFlags.HideAndDontSave);  #else          GameObject go = new GameObject("_UIDrawCall [" + mat.name + "]");          DontDestroyOnLoad(go);  #endif          go.layer = cachedGameObject.layer;                    // Create the draw call          UIDrawCall drawCall = go.AddComponent<UIDrawCall>();          drawCall.baseMaterial = mat;          drawCall.renderQueue = UIDrawCall.list.size;          drawCall.panel = this;          //Debug.Log("Added DC " + mat.name + " as " + UIDrawCall.list.size);          UIDrawCall.list.Add(drawCall);          return drawCall;      }  

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

C#代碼  收藏代碼/// <summary>  /// Fill the geometry fully, processing all widgets and re-creating all draw calls.  /// </summary>    static void Fill ()  {      for (int i = UIDrawCall.list.size; i > 0; )          DestroyDrawCall(UIDrawCall.list[--i], i);        int index = 0;      UIPanel pan = null;      Material mat = null;      UIDrawCall dc = null;        for (int i = 0; i < UIWidget.list.size; )      {          UIWidget w = UIWidget.list[i];            if (w == null)          {              UIWidget.list.RemoveAt(i);              continue;          }            if (w.isVisible && w.hasVertices)          {              if (pan != w.panel || mat != w.material)    //a)              {                  if (pan != null && mat != null && mVerts.size != 0)                  {                      pan.SubmitDrawCall(dc);                      dc = null;                  }                    pan = w.panel;                  mat = w.material;              }                if (pan != null && mat != null)   //b)              {                  if (dc == null) dc = pan.GetDrawCall(index++, mat);                  w.drawCall = dc;                  if (pan.generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);                  else w.WriteToBuffers(mVerts, mUvs, mCols, null, null);              }          }          else w.drawCall = null;          ++i;      }        if (mVerts.size != 0)          pan.SubmitDrawCall(dc);  }  

 整理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#代碼  收藏代碼/// <summary>  /// Main update function  /// </summary>    void LateUpdate ()  {      // Only the very first panel should be doing the update logic      if (list[0] != this) return;        // Update all panels      for (int i = 0; i < list.size; ++i)      {          UIPanel panel = list[i];          panel.mUpdateTime = RealTime.time;          panel.UpdateTransformMatrix();          panel.UpdateLayers();          panel.UpdateWidgets();      }      // Fill the draw calls for all of the changed materials      if (mFullRebuild)      {          UIWidget.list.Sort(UIWidget.CompareFunc);          Fill();      }      else      {          for (int i = 0; i < UIDrawCall.list.size; )          {              UIDrawCall dc = UIDrawCall.list[i];                if (dc.isDirty)              {                  if (!Fill(dc))                  {                      DestroyDrawCall(dc, i);                      continue;                  }              }              ++i;          }      }        // Update the clipping rects      for (int i = 0; i < list.size; ++i)      {          UIPanel panel = list[i];          panel.UpdateDrawcalls();      }      mFullRebuild = false;  }  

        就不進行文字描述了,貼一張自己的畫的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#代碼  收藏代碼/// <summary>  /// Static widget comparison function used for depth sorting.  /// </summary>    static public int CompareFunc (UIWidget left, UIWidget right)  {      int val = UIPanel.CompareFunc(left.mPanel, right.mPanel);        if (val == 0)      {          if (left.mDepth < right.mDepth) return -1;          if (left.mDepth > right.mDepth) return 1;            Material leftMat = left.material;          Material rightMat = right.material;            if (leftMat == rightMat) return 0;          if (leftMat != null) return -1;          if (rightMat != null) return 1;          return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;      }      return val;  }  

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

 

    

 

 

 

 

 

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

C#代碼  收藏代碼/// <summary>  /// Static widget comparison function used for depth sorting.  /// </summary>    static public int CompareFunc (UIWidget left, UIWidget right)  {      int val = UIPanel.CompareFunc(left.mPanel, right.mPanel);        if (val == 0)      {          //原理排序的方法          /*if (left.mDepth < right.mDepth) return -1;         if (left.mDepth > right.mDepth) return 1;          Material leftMat = left.material;         Material rightMat = right.material;          if (leftMat == rightMat) return 0;         if (leftMat != null) return -1;         if (rightMat != null) return 1;         return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;*/                      Material leftMat = left.material;          Material rightMat = right.material;            if (leftMat == rightMat)           {              if (left.mDepth < right.mDepth) return -1;              else if (left.mDepth > right.mDepth) return 1;              else return 0;          }          if(leftMat !=null & rightMat != null)              return string.Compare(leftMat.name,rightMat.name);          if (leftMat != null) return -1;          if (rightMat != null) return 1;          return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;                }      return val;  }  

       最終的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有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@Gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。

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

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


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 义马市| 北宁市| 海原县| 龙州县| 辽阳县| 平远县| 庐江县| 南江县| 高碑店市| 清涧县| 龙泉市| 洪湖市| 英德市| 清镇市| 临潭县| 拉孜县| 来凤县| 永福县| 平江县| 化州市| 仪征市| 兴和县| 西藏| 璧山县| 洮南市| 枝江市| 迭部县| 安远县| 丰宁| 灵丘县| 沅江市| 西平县| 瓮安县| 临漳县| 闸北区| 邯郸市| 个旧市| 长垣县| 阜新| 梁河县| 唐河县|